mirror of
https://github.com/cooperhammond/irs.git
synced 2024-12-22 17:35:28 +00:00
commit
ca02d0bdc7
12
shard.lock
12
shard.lock
|
@ -1,6 +1,10 @@
|
||||||
version: 1.0
|
version: 2.0
|
||||||
shards:
|
shards:
|
||||||
ydl_binaries:
|
json_mapping:
|
||||||
github: cooperhammond/ydl-binaries
|
git: https://github.com/crystal-lang/json_mapping.cr.git
|
||||||
commit: c82e3937fee20fd076b1c73e24b2d0205e2cf0da
|
version: 0.1.0
|
||||||
|
|
||||||
|
ydl_binaries:
|
||||||
|
git: https://github.com/cooperhammond/ydl-binaries.git
|
||||||
|
version: 1.1.1+git.commit.c82e3937fee20fd076b1c73e24b2d0205e2cf0da
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
name: irs
|
name: irs
|
||||||
version: 1.0.0
|
version: 1.0.1
|
||||||
|
|
||||||
authors:
|
authors:
|
||||||
- Cooper Hammond <kepoorh@gmail.com>
|
- Cooper Hammond <kepoorh@gmail.com>
|
||||||
|
@ -13,3 +13,5 @@ license: MIT
|
||||||
dependencies:
|
dependencies:
|
||||||
ydl_binaries:
|
ydl_binaries:
|
||||||
github: cooperhammond/ydl-binaries
|
github: cooperhammond/ydl-binaries
|
||||||
|
json_mapping:
|
||||||
|
github: crystal-lang/json_mapping.cr
|
||||||
|
|
|
@ -9,7 +9,7 @@ class Album < SpotifyList
|
||||||
|
|
||||||
# Uses the `spotify_searcher` defined in parent `SpotifyList` to find the
|
# Uses the `spotify_searcher` defined in parent `SpotifyList` to find the
|
||||||
# correct metadata of the list
|
# correct metadata of the list
|
||||||
def find_it
|
def find_it : JSON::Any
|
||||||
album = @spotify_searcher.find_item("album", {
|
album = @spotify_searcher.find_item("album", {
|
||||||
"name" => @list_name.as(String),
|
"name" => @list_name.as(String),
|
||||||
"artist" => @list_author.as(String),
|
"artist" => @list_author.as(String),
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
require "json"
|
require "json"
|
||||||
|
require "json_mapping"
|
||||||
|
|
||||||
class PlaylistExtensionMapper
|
class PlaylistExtensionMapper
|
||||||
JSON.mapping(
|
JSON.mapping(
|
||||||
|
|
|
@ -13,7 +13,7 @@ class Playlist < SpotifyList
|
||||||
|
|
||||||
# Uses the `spotify_searcher` defined in parent `SpotifyList` to find the
|
# Uses the `spotify_searcher` defined in parent `SpotifyList` to find the
|
||||||
# correct metadata of the list
|
# correct metadata of the list
|
||||||
def find_it
|
def find_it : JSON::Any
|
||||||
@playlist = @spotify_searcher.find_item("playlist", {
|
@playlist = @spotify_searcher.find_item("playlist", {
|
||||||
"name" => @list_name.as(String),
|
"name" => @list_name.as(String),
|
||||||
"username" => @list_author.as(String),
|
"username" => @list_author.as(String),
|
||||||
|
|
|
@ -146,7 +146,8 @@ class Song
|
||||||
FileUtils.mkdir_p(strpath)
|
FileUtils.mkdir_p(strpath)
|
||||||
end
|
end
|
||||||
safe_filename = @filename.gsub(/[\/]/, "").gsub(" ", " ")
|
safe_filename = @filename.gsub(/[\/]/, "").gsub(" ", " ")
|
||||||
File.rename("./" + @filename, (path / safe_filename).to_s)
|
FileUtils.cp("./" + @filename, (path / safe_filename).to_s)
|
||||||
|
FileUtils.rm("./" + @filename)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Provide metadata so that it doesn't have to find it. Useful for overwriting
|
# Provide metadata so that it doesn't have to find it. Useful for overwriting
|
||||||
|
|
157
src/interact/future.cr
Normal file
157
src/interact/future.cr
Normal file
|
@ -0,0 +1,157 @@
|
||||||
|
# copy and pasted from crystal 0.33.1
|
||||||
|
# https://github.com/crystal-lang/crystal/blob/18e76172444c7bd07f58bf360bc21981b667668d/src/concurrent/future.cr#L138
|
||||||
|
|
||||||
|
|
||||||
|
# :nodoc:
|
||||||
|
class Concurrent::Future(R)
|
||||||
|
enum State
|
||||||
|
Idle
|
||||||
|
Delayed
|
||||||
|
Running
|
||||||
|
Completed
|
||||||
|
Canceled
|
||||||
|
end
|
||||||
|
|
||||||
|
@value : R?
|
||||||
|
@error : Exception?
|
||||||
|
@delay : Float64
|
||||||
|
|
||||||
|
def initialize(run_immediately = true, delay = 0.0, &@block : -> R)
|
||||||
|
@state = State::Idle
|
||||||
|
@value = nil
|
||||||
|
@error = nil
|
||||||
|
@channel = Channel(Nil).new
|
||||||
|
@delay = delay.to_f
|
||||||
|
@cancel_msg = nil
|
||||||
|
|
||||||
|
spawn_compute if run_immediately
|
||||||
|
end
|
||||||
|
|
||||||
|
def get
|
||||||
|
wait
|
||||||
|
value_or_raise
|
||||||
|
end
|
||||||
|
|
||||||
|
def success?
|
||||||
|
completed? && !@error
|
||||||
|
end
|
||||||
|
|
||||||
|
def failure?
|
||||||
|
completed? && @error
|
||||||
|
end
|
||||||
|
|
||||||
|
def canceled?
|
||||||
|
@state == State::Canceled
|
||||||
|
end
|
||||||
|
|
||||||
|
def completed?
|
||||||
|
@state == State::Completed
|
||||||
|
end
|
||||||
|
|
||||||
|
def running?
|
||||||
|
@state == State::Running
|
||||||
|
end
|
||||||
|
|
||||||
|
def delayed?
|
||||||
|
@state == State::Delayed
|
||||||
|
end
|
||||||
|
|
||||||
|
def idle?
|
||||||
|
@state == State::Idle
|
||||||
|
end
|
||||||
|
|
||||||
|
def cancel(msg = "Future canceled, you reached the [End of Time]")
|
||||||
|
return if @state >= State::Completed
|
||||||
|
@state = State::Canceled
|
||||||
|
@cancel_msg = msg
|
||||||
|
@channel.close
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
private def compute
|
||||||
|
return if @state >= State::Delayed
|
||||||
|
run_compute
|
||||||
|
end
|
||||||
|
|
||||||
|
private def spawn_compute
|
||||||
|
return if @state >= State::Delayed
|
||||||
|
|
||||||
|
@state = @delay > 0 ? State::Delayed : State::Running
|
||||||
|
|
||||||
|
spawn { run_compute }
|
||||||
|
end
|
||||||
|
|
||||||
|
private def run_compute
|
||||||
|
delay = @delay
|
||||||
|
|
||||||
|
if delay > 0
|
||||||
|
sleep delay
|
||||||
|
return if @state >= State::Canceled
|
||||||
|
@state = State::Running
|
||||||
|
end
|
||||||
|
|
||||||
|
begin
|
||||||
|
@value = @block.call
|
||||||
|
rescue ex
|
||||||
|
@error = ex
|
||||||
|
ensure
|
||||||
|
@channel.close
|
||||||
|
@state = State::Completed
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private def wait
|
||||||
|
return if @state >= State::Completed
|
||||||
|
compute
|
||||||
|
@channel.receive?
|
||||||
|
end
|
||||||
|
|
||||||
|
private def value_or_raise
|
||||||
|
raise Exception.new(@cancel_msg) if @state == State::Canceled
|
||||||
|
|
||||||
|
value = @value
|
||||||
|
if value.is_a?(R)
|
||||||
|
value
|
||||||
|
elsif error = @error
|
||||||
|
raise error
|
||||||
|
else
|
||||||
|
raise "compiler bug"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Spawns a `Fiber` to compute *&block* in the background after *delay* has elapsed.
|
||||||
|
# Access to get is synchronized between fibers. *&block* is only called once.
|
||||||
|
# May be canceled before *&block* is called by calling `cancel`.
|
||||||
|
# ```
|
||||||
|
# d = delay(1) { Process.kill(Process.pid) }
|
||||||
|
# long_operation
|
||||||
|
# d.cancel
|
||||||
|
# ```
|
||||||
|
def delay(delay, &block : -> _)
|
||||||
|
Concurrent::Future.new delay: delay, &block
|
||||||
|
end
|
||||||
|
|
||||||
|
# Spawns a `Fiber` to compute *&block* in the background.
|
||||||
|
# Access to get is synchronized between fibers. *&block* is only called once.
|
||||||
|
# ```
|
||||||
|
# f = future { http_request }
|
||||||
|
# ... other actions ...
|
||||||
|
# f.get #=> String
|
||||||
|
# ```
|
||||||
|
def future(&exp : -> _)
|
||||||
|
Concurrent::Future.new &exp
|
||||||
|
end
|
||||||
|
|
||||||
|
# Conditionally spawns a `Fiber` to run *&block* in the background.
|
||||||
|
# Access to get is synchronized between fibers. *&block* is only called once.
|
||||||
|
# *&block* doesn't run by default, only when `get` is called.
|
||||||
|
# ```
|
||||||
|
# l = lazy { expensive_computation }
|
||||||
|
# spawn { maybe_use_computation(l) }
|
||||||
|
# spawn { maybe_use_computation(l) }
|
||||||
|
# ```
|
||||||
|
def lazy(&block : -> _)
|
||||||
|
Concurrent::Future.new run_immediately: false, &block
|
||||||
|
end
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
require "./future"
|
||||||
|
|
||||||
class Logger
|
class Logger
|
||||||
@done_signal = "---DONE---"
|
@done_signal = "---DONE---"
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
require "http"
|
require "http"
|
||||||
require "xml"
|
require "xml"
|
||||||
|
require "json"
|
||||||
|
|
||||||
|
|
||||||
module Youtube
|
module Youtube
|
||||||
extend self
|
extend self
|
||||||
|
@ -19,6 +21,8 @@ module Youtube
|
||||||
"official video", "official music video",
|
"official video", "official music video",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
alias NODES_CLASS = Array(Hash(String, String))
|
||||||
|
|
||||||
# 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>"
|
||||||
|
@ -63,7 +67,7 @@ module Youtube
|
||||||
# ...
|
# ...
|
||||||
# ]
|
# ]
|
||||||
private def rank_videos(song_name : String, artist_name : String,
|
private def rank_videos(song_name : String, artist_name : String,
|
||||||
query : String, nodes : Array(XML::Node)) : Array(Hash(String, Int32))
|
query : String, nodes : Array(Hash(String, String))) : Array(Hash(String, Int32))
|
||||||
points = [] of Hash(String, Int32)
|
points = [] of Hash(String, Int32)
|
||||||
index = 0
|
index = 0
|
||||||
|
|
||||||
|
@ -149,32 +153,45 @@ module Youtube
|
||||||
|
|
||||||
# Finds valid video links from a `HTTP::Client.get` request
|
# Finds valid video links from a `HTTP::Client.get` request
|
||||||
# Returns an `Array` of `XML::Node`
|
# Returns an `Array` of `XML::Node`
|
||||||
private def get_video_link_nodes(doc : String) : Array(XML::Node)
|
private def get_video_link_nodes(response_body : String) : NODES_CLASS
|
||||||
nodes = XML.parse(doc).xpath_nodes("//a")
|
yt_initial_data : JSON::Any = JSON.parse("{}")
|
||||||
valid_nodes = [] of XML::Node
|
|
||||||
|
|
||||||
nodes.each do |node|
|
response_body.each_line do |line|
|
||||||
if video_link_node?(node)
|
if line.includes?("window[\"ytInitialData\"]")
|
||||||
valid_nodes.push(node)
|
yt_initial_data = JSON.parse(line.split(" = ")[1][0..-2])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
return valid_nodes
|
if yt_initial_data == JSON.parse("{}")
|
||||||
end
|
puts "Youtube has changed the way it organizes its webpage, submit a bug"
|
||||||
|
puts "on https://github.com/cooperhammond/irs"
|
||||||
# Tests if the provided `XML::Node` has a valid link to a video
|
exit(1)
|
||||||
# Returns a `Bool`
|
|
||||||
private def video_link_node?(node : XML::Node) : Bool
|
|
||||||
# If this passes, then the node links to a playlist, not a video
|
|
||||||
if node["href"]?
|
|
||||||
return false if node["href"].includes?("&list=")
|
|
||||||
end
|
end
|
||||||
|
|
||||||
VALID_LINK_CLASSES.each do |valid_class|
|
# where the vid metadata lives
|
||||||
if node["class"]?
|
yt_initial_data = yt_initial_data["contents"]["twoColumnSearchResultsRenderer"]["primaryContents"]["sectionListRenderer"]["contents"]
|
||||||
return true if node["class"].includes?(valid_class)
|
|
||||||
|
video_metadata = [] of Hash(String, String)
|
||||||
|
|
||||||
|
i = 0
|
||||||
|
while true
|
||||||
|
begin
|
||||||
|
# video title
|
||||||
|
raw_metadata = yt_initial_data[0]["itemSectionRenderer"]["contents"][i]["videoRenderer"]
|
||||||
|
|
||||||
|
metadata = {} of String => String
|
||||||
|
|
||||||
|
metadata["title"] = raw_metadata["title"]["runs"][0]["text"].as_s
|
||||||
|
metadata["href"] = raw_metadata["navigationEndpoint"]["commandMetadata"]["webCommandMetadata"]["url"].as_s
|
||||||
|
|
||||||
|
video_metadata.push(metadata)
|
||||||
|
rescue IndexError
|
||||||
|
break
|
||||||
|
rescue Exception
|
||||||
end
|
end
|
||||||
|
i += 1
|
||||||
end
|
end
|
||||||
return false
|
|
||||||
|
return video_metadata
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue