Merge pull request #66 from Who23/youtube-sources

Add ability to specifiy youtube URLs manually

Looks like wonderful stuff, I appreciate the contribution and time investment to my small homebrew project, I hope you've been getting good stuff out of it.
This commit is contained in:
Cooper Hammond 2020-09-08 13:54:58 -06:00 committed by GitHub
commit 58895e2e87
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 89 additions and 15 deletions

View file

@ -54,6 +54,8 @@ Arguments:
-s, --song <song> Specify song name to download -s, --song <song> Specify song name to download
-A, --album <album> Specify the album name to download -A, --album <album> Specify the album name to download
-p, --playlist <playlist> Specify the playlist name to download -p, --playlist <playlist> Specify the playlist name to download
-u, --url <url> Specify the youtube url to download from (for single songs only)
-g, --give-url Specify the youtube url sources while downloading (for albums or playlists only only)
Examples: Examples:
$ irs --song "Bohemian Rhapsody" --artist "Queen" $ irs --song "Bohemian Rhapsody" --artist "Queen"

View file

@ -20,6 +20,8 @@ class CLI
[["-s", "--song"], "song", "string"], [["-s", "--song"], "song", "string"],
[["-A", "--album"], "album", "string"], [["-A", "--album"], "album", "string"],
[["-p", "--playlist"], "playlist", "string"], [["-p", "--playlist"], "playlist", "string"],
[["-u", "--url"], "url", "string"],
[["-g", "--give-url"], "give-url", "bool"],
] ]
@args : Hash(String, String) @args : Hash(String, String)
@ -48,6 +50,8 @@ class CLI
#{Style.blue "-s, --song <song>"} Specify song name to download #{Style.blue "-s, --song <song>"} Specify song name to download
#{Style.blue "-A, --album <album>"} Specify the album name to download #{Style.blue "-A, --album <album>"} Specify the album name to download
#{Style.blue "-p, --playlist <playlist>"} Specify the playlist name to download #{Style.blue "-p, --playlist <playlist>"} Specify the playlist name to download
#{Style.blue "-u, --url <url>"} Specify the youtube url to download from (for single songs only)
#{Style.blue "-g, --give-url"} Specify the youtube url sources while downloading (for albums or playlists only)
#{Style.bold "Examples:"} #{Style.bold "Examples:"}
$ #{Style.green %(irs --song "Bohemian Rhapsody" --artist "Queen")} $ #{Style.green %(irs --song "Bohemian Rhapsody" --artist "Queen")}
@ -82,17 +86,17 @@ class CLI
elsif @args["song"]? && @args["artist"]? elsif @args["song"]? && @args["artist"]?
s = Song.new(@args["song"], @args["artist"]) s = Song.new(@args["song"], @args["artist"])
s.provide_client_keys(Config.client_key, Config.client_secret) s.provide_client_keys(Config.client_key, Config.client_secret)
s.grab_it s.grab_it(@args["url"]?)
s.organize_it(Config.music_directory) s.organize_it(Config.music_directory)
exit exit
elsif @args["album"]? && @args["artist"]? elsif @args["album"]? && @args["artist"]?
a = Album.new(@args["album"], @args["artist"]) a = Album.new(@args["album"], @args["artist"])
a.provide_client_keys(Config.client_key, Config.client_secret) a.provide_client_keys(Config.client_key, Config.client_secret)
a.grab_it a.grab_it(!!@args["give-url"])
elsif @args["playlist"]? && @args["artist"]? elsif @args["playlist"]? && @args["artist"]?
p = Playlist.new(@args["playlist"], @args["artist"]) p = Playlist.new(@args["playlist"], @args["artist"])
p.provide_client_keys(Config.client_key, Config.client_secret) p.provide_client_keys(Config.client_key, Config.client_secret)
p.grab_it p.grab_it(!!@args["give-url"])
else else
puts Style.red("Those arguments don't do anything when used that way.") puts Style.red("Those arguments don't do anything when used that way.")
puts "Type `irs -h` to see usage." puts "Type `irs -h` to see usage."

View file

@ -17,6 +17,9 @@ abstract class SpotifyList
"searching" => [ "searching" => [
Style.bold("Searching for %l by %a ... \r"), Style.bold("Searching for %l by %a ... \r"),
Style.green("+ ") + Style.bold("%l by %a \n") Style.green("+ ") + Style.bold("%l by %a \n")
],
"url" => [
Style.bold("When prompted for a URL, provide a youtube URL or press enter to scrape for one\n")
] ]
} }
@ -24,11 +27,15 @@ abstract class SpotifyList
end end
# Finds the list, and downloads all of the songs using the `Song` class # Finds the list, and downloads all of the songs using the `Song` class
def grab_it def grab_it(ask_url : Bool = false)
if !@spotify_searcher.authorized? if !@spotify_searcher.authorized?
raise("Need to call provide_client_keys on Album or Playlist class.") raise("Need to call provide_client_keys on Album or Playlist class.")
end end
if ask_url
outputter("url", 0)
end
outputter("searching", 0) outputter("searching", 0)
list = find_it() list = find_it()
outputter("searching", 1) outputter("searching", 1)
@ -47,7 +54,7 @@ abstract class SpotifyList
song.provide_metadata(data) song.provide_metadata(data)
puts Style.bold("[#{data["track_number"]}/#{contents.size}]") puts Style.bold("[#{data["track_number"]}/#{contents.size}]")
song.grab_it song.grab_it ask_url: ask_url
organize(song) organize(song)

View file

@ -24,7 +24,10 @@ class Song
], ],
"url" => [ "url" => [
" Searching for URL ...\r", " Searching for URL ...\r",
Style.green(" + ") + Style.dim("URL found \n") Style.green(" + ") + Style.dim("URL found \n"),
" Validating URL ...\r",
Style.green(" + ") + Style.dim("URL validated \n"),
"URL?: "
], ],
"download" => [ "download" => [
" Downloading video:\n", " Downloading video:\n",
@ -47,11 +50,12 @@ class Song
end end
# Find, downloads, and tags the mp3 song that this class represents. # Find, downloads, and tags the mp3 song that this class represents.
# Optionally takes a youtube URL to download from
# #
# ``` # ```
# Song.new("Bohemian Rhapsody", "Queen").grab_it # Song.new("Bohemian Rhapsody", "Queen").grab_it
# ``` # ```
def grab_it def grab_it(url : (String | Nil) = nil, ask_url : Bool = false)
outputter("intro", 0) outputter("intro", 0)
if !@spotify_searcher.authorized? && !@metadata if !@spotify_searcher.authorized? && !@metadata
@ -81,14 +85,31 @@ class Song
data = @metadata.as(JSON::Any) data = @metadata.as(JSON::Any)
@filename = data["track_number"].to_s + " - #{data["name"].to_s}.mp3" @filename = data["track_number"].to_s + " - #{data["name"].to_s}.mp3"
outputter("url", 0) if ask_url
url = Youtube.find_url(@song_name, @artist_name, search_terms: "lyrics") outputter("url", 4)
if !url url = gets
raise("There was no url found on youtube for " + if !url.nil? && url.strip == ""
%("#{@song_name}" by "#{@artist_name}. ) + url = nil
"Check your input and try again.") end
end
if !url
outputter("url", 0)
url = Youtube.find_url(@song_name, @artist_name, search_terms: "lyrics")
if !url
raise("There was no url found on youtube for " +
%("#{@song_name}" by "#{@artist_name}. ) +
"Check your input and try again.")
end
outputter("url", 1)
else
outputter("url", 2)
if !Youtube.is_valid_url(url)
raise("The url '#{url}' is an invalid youtube URL " +
"Check the URL and try again")
end
outputter("url", 3)
end end
outputter("url", 1)
outputter("download", 0) outputter("download", 0)
Ripper.download_mp3(url.as(String), @filename) Ripper.download_mp3(url.as(String), @filename)

View file

@ -1,6 +1,7 @@
require "http" require "http"
require "xml" require "xml"
require "json" require "json"
require "uri"
module Youtube module Youtube
@ -23,6 +24,45 @@ module Youtube
alias NODES_CLASS = Array(Hash(String, String)) alias NODES_CLASS = Array(Hash(String, String))
# Checks if the given URL is a valid youtube URL
#
# ```
# Youtube.is_valid_url("https://www.youtube.com/watch?v=NOTANACTUALVIDEOID")
# => false
# ```
def is_valid_url(url : String) : Bool
uri = URI.parse url
# is it a video on youtube, with a query
query = uri.query
if uri.host != "www.youtube.com" || uri.path != "/watch" || !query
return false
end
queries = query.split('&')
# find the video ID
i = 0
while i < queries.size
if queries[i].starts_with?("v=")
vID = queries[i][2..-1]
break
end
i += 1
end
if !vID
return false
end
# this is an internal endpoint to validate the video ID
response = HTTP::Client.get "https://www.youtube.com/get_video_info?video_id=#{vID}"
return response.body.includes?("status=ok")
end
# Finds a youtube url based off of the given information. # Finds a youtube url based off of the given information.
# The query to youtube is constructed like this: # The query to youtube is constructed like this:
# "<song_name> <artist_name> <search terms>" # "<song_name> <artist_name> <search terms>"