Skip to content

Commit ca02d0b

Browse files
Merge pull request #64 from cooperhammond/update-to-.35
Update to .35
2 parents 4e1ce85 + 0f32ec6 commit ca02d0b

File tree

9 files changed

+212
-28
lines changed

9 files changed

+212
-28
lines changed

shard.lock

+7-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1-
version: 1.0
1+
version: 2.0
22
shards:
3+
json_mapping:
4+
git: https://github.com/crystal-lang/json_mapping.cr.git
5+
version: 0.1.0
6+
37
ydl_binaries:
4-
github: cooperhammond/ydl-binaries
5-
commit: c82e3937fee20fd076b1c73e24b2d0205e2cf0da
8+
git: https://github.com/cooperhammond/ydl-binaries.git
9+
version: 1.1.1+git.commit.c82e3937fee20fd076b1c73e24b2d0205e2cf0da
610

shard.yml

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: irs
2-
version: 1.0.0
2+
version: 1.0.1
33

44
authors:
55
- Cooper Hammond <[email protected]>
@@ -12,4 +12,6 @@ license: MIT
1212

1313
dependencies:
1414
ydl_binaries:
15-
github: cooperhammond/ydl-binaries
15+
github: cooperhammond/ydl-binaries
16+
json_mapping:
17+
github: crystal-lang/json_mapping.cr

src/glue/album.cr

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ class Album < SpotifyList
99

1010
# Uses the `spotify_searcher` defined in parent `SpotifyList` to find the
1111
# correct metadata of the list
12-
def find_it
12+
def find_it : JSON::Any
1313
album = @spotify_searcher.find_item("album", {
1414
"name" => @list_name.as(String),
1515
"artist" => @list_author.as(String),

src/glue/mapper.cr

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
require "json"
2+
require "json_mapping"
23

34
class PlaylistExtensionMapper
45
JSON.mapping(

src/glue/playlist.cr

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ class Playlist < SpotifyList
1313

1414
# Uses the `spotify_searcher` defined in parent `SpotifyList` to find the
1515
# correct metadata of the list
16-
def find_it
16+
def find_it : JSON::Any
1717
@playlist = @spotify_searcher.find_item("playlist", {
1818
"name" => @list_name.as(String),
1919
"username" => @list_author.as(String),

src/glue/song.cr

+2-1
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,8 @@ class Song
146146
FileUtils.mkdir_p(strpath)
147147
end
148148
safe_filename = @filename.gsub(/[\/]/, "").gsub(" ", " ")
149-
File.rename("./" + @filename, (path / safe_filename).to_s)
149+
FileUtils.cp("./" + @filename, (path / safe_filename).to_s)
150+
FileUtils.rm("./" + @filename)
150151
end
151152

152153
# Provide metadata so that it doesn't have to find it. Useful for overwriting

src/interact/future.cr

+157
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
# copy and pasted from crystal 0.33.1
2+
# https://github.com/crystal-lang/crystal/blob/18e76172444c7bd07f58bf360bc21981b667668d/src/concurrent/future.cr#L138
3+
4+
5+
# :nodoc:
6+
class Concurrent::Future(R)
7+
enum State
8+
Idle
9+
Delayed
10+
Running
11+
Completed
12+
Canceled
13+
end
14+
15+
@value : R?
16+
@error : Exception?
17+
@delay : Float64
18+
19+
def initialize(run_immediately = true, delay = 0.0, &@block : -> R)
20+
@state = State::Idle
21+
@value = nil
22+
@error = nil
23+
@channel = Channel(Nil).new
24+
@delay = delay.to_f
25+
@cancel_msg = nil
26+
27+
spawn_compute if run_immediately
28+
end
29+
30+
def get
31+
wait
32+
value_or_raise
33+
end
34+
35+
def success?
36+
completed? && !@error
37+
end
38+
39+
def failure?
40+
completed? && @error
41+
end
42+
43+
def canceled?
44+
@state == State::Canceled
45+
end
46+
47+
def completed?
48+
@state == State::Completed
49+
end
50+
51+
def running?
52+
@state == State::Running
53+
end
54+
55+
def delayed?
56+
@state == State::Delayed
57+
end
58+
59+
def idle?
60+
@state == State::Idle
61+
end
62+
63+
def cancel(msg = "Future canceled, you reached the [End of Time]")
64+
return if @state >= State::Completed
65+
@state = State::Canceled
66+
@cancel_msg = msg
67+
@channel.close
68+
nil
69+
end
70+
71+
private def compute
72+
return if @state >= State::Delayed
73+
run_compute
74+
end
75+
76+
private def spawn_compute
77+
return if @state >= State::Delayed
78+
79+
@state = @delay > 0 ? State::Delayed : State::Running
80+
81+
spawn { run_compute }
82+
end
83+
84+
private def run_compute
85+
delay = @delay
86+
87+
if delay > 0
88+
sleep delay
89+
return if @state >= State::Canceled
90+
@state = State::Running
91+
end
92+
93+
begin
94+
@value = @block.call
95+
rescue ex
96+
@error = ex
97+
ensure
98+
@channel.close
99+
@state = State::Completed
100+
end
101+
end
102+
103+
private def wait
104+
return if @state >= State::Completed
105+
compute
106+
@channel.receive?
107+
end
108+
109+
private def value_or_raise
110+
raise Exception.new(@cancel_msg) if @state == State::Canceled
111+
112+
value = @value
113+
if value.is_a?(R)
114+
value
115+
elsif error = @error
116+
raise error
117+
else
118+
raise "compiler bug"
119+
end
120+
end
121+
end
122+
123+
# Spawns a `Fiber` to compute *&block* in the background after *delay* has elapsed.
124+
# Access to get is synchronized between fibers. *&block* is only called once.
125+
# May be canceled before *&block* is called by calling `cancel`.
126+
# ```
127+
# d = delay(1) { Process.kill(Process.pid) }
128+
# long_operation
129+
# d.cancel
130+
# ```
131+
def delay(delay, &block : -> _)
132+
Concurrent::Future.new delay: delay, &block
133+
end
134+
135+
# Spawns a `Fiber` to compute *&block* in the background.
136+
# Access to get is synchronized between fibers. *&block* is only called once.
137+
# ```
138+
# f = future { http_request }
139+
# ... other actions ...
140+
# f.get #=> String
141+
# ```
142+
def future(&exp : -> _)
143+
Concurrent::Future.new &exp
144+
end
145+
146+
# Conditionally spawns a `Fiber` to run *&block* in the background.
147+
# Access to get is synchronized between fibers. *&block* is only called once.
148+
# *&block* doesn't run by default, only when `get` is called.
149+
# ```
150+
# l = lazy { expensive_computation }
151+
# spawn { maybe_use_computation(l) }
152+
# spawn { maybe_use_computation(l) }
153+
# ```
154+
def lazy(&block : -> _)
155+
Concurrent::Future.new run_immediately: false, &block
156+
end
157+

src/interact/logger.cr

+2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
require "./future"
2+
13
class Logger
24
@done_signal = "---DONE---"
35

src/search/youtube.cr

+37-20
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
require "http"
22
require "xml"
3+
require "json"
4+
35

46
module Youtube
57
extend self
@@ -19,6 +21,8 @@ module Youtube
1921
"official video", "official music video",
2022
]
2123

24+
alias NODES_CLASS = Array(Hash(String, String))
25+
2226
# Finds a youtube url based off of the given information.
2327
# The query to youtube is constructed like this:
2428
# "<song_name> <artist_name> <search terms>"
@@ -63,7 +67,7 @@ module Youtube
6367
# ...
6468
# ]
6569
private def rank_videos(song_name : String, artist_name : String,
66-
query : String, nodes : Array(XML::Node)) : Array(Hash(String, Int32))
70+
query : String, nodes : Array(Hash(String, String))) : Array(Hash(String, Int32))
6771
points = [] of Hash(String, Int32)
6872
index = 0
6973

@@ -149,32 +153,45 @@ module Youtube
149153

150154
# Finds valid video links from a `HTTP::Client.get` request
151155
# Returns an `Array` of `XML::Node`
152-
private def get_video_link_nodes(doc : String) : Array(XML::Node)
153-
nodes = XML.parse(doc).xpath_nodes("//a")
154-
valid_nodes = [] of XML::Node
156+
private def get_video_link_nodes(response_body : String) : NODES_CLASS
157+
yt_initial_data : JSON::Any = JSON.parse("{}")
155158

156-
nodes.each do |node|
157-
if video_link_node?(node)
158-
valid_nodes.push(node)
159+
response_body.each_line do |line|
160+
if line.includes?("window[\"ytInitialData\"]")
161+
yt_initial_data = JSON.parse(line.split(" = ")[1][0..-2])
159162
end
160163
end
161164

162-
return valid_nodes
163-
end
164-
165-
# Tests if the provided `XML::Node` has a valid link to a video
166-
# Returns a `Bool`
167-
private def video_link_node?(node : XML::Node) : Bool
168-
# If this passes, then the node links to a playlist, not a video
169-
if node["href"]?
170-
return false if node["href"].includes?("&list=")
165+
if yt_initial_data == JSON.parse("{}")
166+
puts "Youtube has changed the way it organizes its webpage, submit a bug"
167+
puts "on https://github.com/cooperhammond/irs"
168+
exit(1)
171169
end
172170

173-
VALID_LINK_CLASSES.each do |valid_class|
174-
if node["class"]?
175-
return true if node["class"].includes?(valid_class)
171+
# where the vid metadata lives
172+
yt_initial_data = yt_initial_data["contents"]["twoColumnSearchResultsRenderer"]["primaryContents"]["sectionListRenderer"]["contents"]
173+
174+
video_metadata = [] of Hash(String, String)
175+
176+
i = 0
177+
while true
178+
begin
179+
# video title
180+
raw_metadata = yt_initial_data[0]["itemSectionRenderer"]["contents"][i]["videoRenderer"]
181+
182+
metadata = {} of String => String
183+
184+
metadata["title"] = raw_metadata["title"]["runs"][0]["text"].as_s
185+
metadata["href"] = raw_metadata["navigationEndpoint"]["commandMetadata"]["webCommandMetadata"]["url"].as_s
186+
187+
video_metadata.push(metadata)
188+
rescue IndexError
189+
break
190+
rescue Exception
176191
end
192+
i += 1
177193
end
178-
return false
194+
195+
return video_metadata
179196
end
180197
end

0 commit comments

Comments
 (0)