forked from midou/invidious
Compare commits
74 Commits
Author | SHA1 | Date | |
---|---|---|---|
044a57ef34 | |||
bc49c7d181 | |||
5632e58636 | |||
e1bf7fa6cc | |||
40028e1462 | |||
53732cdcab | |||
2ac89d5e00 | |||
98d71ca8e7 | |||
0f2f273335 | |||
000cfd4834 | |||
25c3ee034e | |||
89d3587861 | |||
0d8f036bf1 | |||
81c520e0dd | |||
c0bda13965 | |||
3b1df75061 | |||
eda5beaed5 | |||
4022670cb1 | |||
7b135a6d0c | |||
bdaa8a06fd | |||
b3f9059452 | |||
917d220623 | |||
ed8ddbc07d | |||
cb01b50fbb | |||
6b3c9d23d0 | |||
3839013a37 | |||
9d5dddab29 | |||
45fa148380 | |||
2ba0063dc0 | |||
b57176d7ef | |||
0dbef6ab9f | |||
8fc4dcfdea | |||
6c98513153 | |||
c3d8ca68b3 | |||
a37692cce4 | |||
a1ad561b98 | |||
7fd0f93d02 | |||
23aaf7f1b7 | |||
41a04e7c67 | |||
77b12b6249 | |||
78fcf579a7 | |||
9ae3bf216e | |||
0e7c56687b | |||
01a80995d3 | |||
76d3abb5f9 | |||
deb4b06ea0 | |||
4725f7222b | |||
16c7d99dd8 | |||
55f8fd0b58 | |||
1611ee83a6 | |||
567b9f31f3 | |||
6bb747b579 | |||
9a15438c71 | |||
4760b3c6e7 | |||
9e68df965b | |||
3ba2a7d921 | |||
71aa4d0347 | |||
bb0b60e575 | |||
fa2ba807a3 | |||
bce01cba32 | |||
ec399f5f7b | |||
7c63c759f4 | |||
b72f3c2274 | |||
74cf3d18d0 | |||
8adb4650a0 | |||
45ce301bd2 | |||
d9ea8e413e | |||
2cedac8c58 | |||
c5bd5e6c6d | |||
7dfb301858 | |||
f26e9313ff | |||
1409160ee6 | |||
6e434409a0 | |||
3833366756 |
22
README.md
22
README.md
@ -1,7 +1,27 @@
|
|||||||
# Invidious
|
# Invidious
|
||||||
|
|
||||||
## Invidious is what YouTube should be
|
## Invidious is an alternative front-end to YouTube
|
||||||
|
|
||||||
|
- Audio-only (and no need to keep window open on mobile)
|
||||||
|
- [Open-source](https://github.com/omarroth/invidious) (AGPLv3 licensed)
|
||||||
|
- No ads
|
||||||
|
- No need to create a Google account to save subscriptions
|
||||||
|
- Lightweight (homepage is ~4 KB compressed)
|
||||||
|
- Tools for managing subscriptions:
|
||||||
|
- Only show unseen videos
|
||||||
|
- Only show latest (or latest unseen) video from each channel
|
||||||
|
- Delivers notifications from all subscribed channels
|
||||||
|
- Automatically redirect homepage to feed
|
||||||
|
- Import subscriptions from YouTube
|
||||||
|
- Dark mode
|
||||||
|
- Embed support
|
||||||
|
- Set default player options (speed, quality, autoplay, loop)
|
||||||
|
- Does not require JS to play videos
|
||||||
|
- Support for Reddit comments in place of YT comments
|
||||||
|
- Import/Export subscriptions, watch history, preference
|
||||||
|
- Does not use any of the official YouTube APIs
|
||||||
|
|
||||||
|
Liberapay: https://liberapay.com/omarroth
|
||||||
Patreon: https://patreon.com/omarroth
|
Patreon: https://patreon.com/omarroth
|
||||||
BTC: 356DpZyMXu6rYd55Yqzjs29n79kGKWcYrY
|
BTC: 356DpZyMXu6rYd55Yqzjs29n79kGKWcYrY
|
||||||
BCH: qq4ptclkzej5eza6a50et5ggc58hxsq5aylqut2npk
|
BCH: qq4ptclkzej5eza6a50et5ggc58hxsq5aylqut2npk
|
||||||
|
@ -171,6 +171,11 @@ div {
|
|||||||
background-color: rgba(0, 182, 240, 1);
|
background-color: rgba(0, 182, 240, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ProgressBar marker */
|
||||||
|
.vjs-marker {
|
||||||
|
background-color: rgba(255, 255, 255, 1);
|
||||||
|
}
|
||||||
|
|
||||||
/* Big "Play" Button */
|
/* Big "Play" Button */
|
||||||
.video-js .vjs-big-play-button {
|
.video-js .vjs-big-play-button {
|
||||||
background-color: rgba(35, 35, 35, 0.5);
|
background-color: rgba(35, 35, 35, 0.5);
|
||||||
|
@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
-- DROP TABLE public.users;
|
-- DROP TABLE public.users;
|
||||||
|
|
||||||
CREATE TABLE public.users
|
CREATE TABLE public.users
|
||||||
(
|
(
|
||||||
id text COLLATE pg_catalog."default" NOT NULL,
|
id text[] COLLATE pg_catalog."default" NOT NULL,
|
||||||
updated timestamp with time zone,
|
updated timestamp with time zone,
|
||||||
notifications text[] COLLATE pg_catalog."default",
|
notifications text[] COLLATE pg_catalog."default",
|
||||||
subscriptions text[] COLLATE pg_catalog."default",
|
subscriptions text[] COLLATE pg_catalog."default",
|
||||||
@ -13,8 +13,7 @@ CREATE TABLE public.users
|
|||||||
password text COLLATE pg_catalog."default",
|
password text COLLATE pg_catalog."default",
|
||||||
token text COLLATE pg_catalog."default",
|
token text COLLATE pg_catalog."default",
|
||||||
watched text[] COLLATE pg_catalog."default",
|
watched text[] COLLATE pg_catalog."default",
|
||||||
CONSTRAINT users_email_key UNIQUE (email),
|
CONSTRAINT users_email_key UNIQUE (email)
|
||||||
CONSTRAINT users_id_key UNIQUE (id)
|
|
||||||
)
|
)
|
||||||
WITH (
|
WITH (
|
||||||
OIDS = FALSE
|
OIDS = FALSE
|
||||||
|
@ -20,6 +20,7 @@ CREATE TABLE public.videos
|
|||||||
allowed_regions text[] COLLATE pg_catalog."default",
|
allowed_regions text[] COLLATE pg_catalog."default",
|
||||||
is_family_friendly boolean,
|
is_family_friendly boolean,
|
||||||
genre text COLLATE pg_catalog."default",
|
genre text COLLATE pg_catalog."default",
|
||||||
|
genre_url text COLLATE pg_catalog."default",
|
||||||
CONSTRAINT videos_pkey PRIMARY KEY (id)
|
CONSTRAINT videos_pkey PRIMARY KEY (id)
|
||||||
)
|
)
|
||||||
WITH (
|
WITH (
|
||||||
|
@ -11,14 +11,11 @@ targets:
|
|||||||
dependencies:
|
dependencies:
|
||||||
kemal:
|
kemal:
|
||||||
github: kemalcr/kemal
|
github: kemalcr/kemal
|
||||||
branch: rework-param-parser
|
|
||||||
pg:
|
pg:
|
||||||
github: will/crystal-pg
|
github: will/crystal-pg
|
||||||
branch: master
|
|
||||||
detect_language:
|
detect_language:
|
||||||
github: detectlanguage/detectlanguage-crystal
|
github: detectlanguage/detectlanguage-crystal
|
||||||
branch: master
|
|
||||||
|
|
||||||
crystal: 0.25.1
|
crystal: 0.26.0
|
||||||
|
|
||||||
license: AGPLv3
|
license: AGPLv3
|
||||||
|
680
src/invidious.cr
680
src/invidious.cr
File diff suppressed because it is too large
Load Diff
@ -73,7 +73,7 @@ def fetch_channel(ucid, client, db, pull_all_videos = true)
|
|||||||
page = 1
|
page = 1
|
||||||
|
|
||||||
loop do
|
loop do
|
||||||
url = produce_videos_url(ucid, page)
|
url = produce_channel_videos_url(ucid, page)
|
||||||
response = client.get(url)
|
response = client.get(url)
|
||||||
|
|
||||||
json = JSON.parse(response.body)
|
json = JSON.parse(response.body)
|
||||||
@ -130,3 +130,45 @@ def fetch_channel(ucid, client, db, pull_all_videos = true)
|
|||||||
|
|
||||||
return channel
|
return channel
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def produce_channel_videos_url(ucid, page = 1, auto_generated = nil)
|
||||||
|
if auto_generated
|
||||||
|
seed = Time.epoch(1525757349)
|
||||||
|
|
||||||
|
until seed >= Time.now
|
||||||
|
seed += 1.month
|
||||||
|
end
|
||||||
|
timestamp = seed - (page - 1).months
|
||||||
|
|
||||||
|
page = "#{timestamp.epoch}"
|
||||||
|
switch = "\x36"
|
||||||
|
else
|
||||||
|
page = "#{page}"
|
||||||
|
switch = "\x00"
|
||||||
|
end
|
||||||
|
|
||||||
|
meta = "\x12\x06videos #{switch}\x30\x02\x38\x01\x60\x01\x6a\x00\x7a"
|
||||||
|
meta += page.size.to_u8.unsafe_chr
|
||||||
|
meta += page
|
||||||
|
meta += "\xb8\x01\x00"
|
||||||
|
|
||||||
|
meta = Base64.urlsafe_encode(meta)
|
||||||
|
meta = URI.escape(meta)
|
||||||
|
|
||||||
|
continuation = "\x12"
|
||||||
|
continuation += ucid.size.to_u8.unsafe_chr
|
||||||
|
continuation += ucid
|
||||||
|
continuation += "\x1a"
|
||||||
|
continuation += meta.size.to_u8.unsafe_chr
|
||||||
|
continuation += meta
|
||||||
|
|
||||||
|
continuation = continuation.size.to_u8.unsafe_chr + continuation
|
||||||
|
continuation = "\xe2\xa9\x85\xb2\x02" + continuation
|
||||||
|
|
||||||
|
continuation = Base64.urlsafe_encode(continuation)
|
||||||
|
continuation = URI.escape(continuation)
|
||||||
|
|
||||||
|
url = "/browse_ajax?continuation=#{continuation}"
|
||||||
|
|
||||||
|
return url
|
||||||
|
end
|
||||||
|
@ -93,7 +93,7 @@ def template_youtube_comments(comments)
|
|||||||
<div class="pure-u-23-24">
|
<div class="pure-u-23-24">
|
||||||
<p>
|
<p>
|
||||||
<a href="javascript:void(0)" data-continuation="#{child["replies"]["continuation"]}"
|
<a href="javascript:void(0)" data-continuation="#{child["replies"]["continuation"]}"
|
||||||
onclick="load_comments(this)">View #{child["replies"]["replyCount"]} replies</a>
|
onclick="get_youtube_replies(this)">View #{child["replies"]["replyCount"]} replies</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -103,7 +103,7 @@ def template_youtube_comments(comments)
|
|||||||
html += <<-END_HTML
|
html += <<-END_HTML
|
||||||
<div class="pure-g">
|
<div class="pure-g">
|
||||||
<div class="pure-u-2-24">
|
<div class="pure-u-2-24">
|
||||||
<img style="width:90%; padding-right:1em; padding-top:1em;" src="#{child["authorThumbnails"][0]["url"]}">
|
<img style="width:90%; padding-right:1em; padding-top:1em;" src="#{child["authorThumbnails"][-1]["url"]}">
|
||||||
</div>
|
</div>
|
||||||
<div class="pure-u-22-24">
|
<div class="pure-u-22-24">
|
||||||
<p>
|
<p>
|
||||||
@ -113,7 +113,7 @@ def template_youtube_comments(comments)
|
|||||||
- #{recode_date(Time.epoch(child["published"].as_i64))} ago
|
- #{recode_date(Time.epoch(child["published"].as_i64))} ago
|
||||||
</p>
|
</p>
|
||||||
<div>
|
<div>
|
||||||
#{child["content"]}
|
<p style="white-space:pre-wrap">#{child["contentHtml"]}</p>
|
||||||
#{replies_html}
|
#{replies_html}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -127,7 +127,7 @@ def template_youtube_comments(comments)
|
|||||||
<div class="pure-u-1">
|
<div class="pure-u-1">
|
||||||
<p>
|
<p>
|
||||||
<a href="javascript:void(0)" data-continuation="#{comments["continuation"]}"
|
<a href="javascript:void(0)" data-continuation="#{comments["continuation"]}"
|
||||||
onclick="load_comments(this)">Load more</a>
|
onclick="get_youtube_replies(this)">Load more</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -190,37 +190,21 @@ def template_reddit_comments(root)
|
|||||||
return html
|
return html
|
||||||
end
|
end
|
||||||
|
|
||||||
def add_alt_links(html)
|
def replace_links(html)
|
||||||
alt_links = [] of {String, String}
|
html = XML.parse_html(html)
|
||||||
|
|
||||||
# This is painful but likely the only way to accomplish this in Crystal,
|
html.xpath_nodes(%q(//a)).each do |anchor|
|
||||||
# as Crystigiri and others are not able to insert XML Nodes into a document.
|
|
||||||
# The goal here is to use as little regex as possible
|
|
||||||
html.scan(/<a[^>]*>([^<]+)<\/a>/) do |match|
|
|
||||||
anchor = XML.parse_html(match[0])
|
|
||||||
anchor = anchor.xpath_node("//a").not_nil!
|
|
||||||
url = URI.parse(anchor["href"])
|
url = URI.parse(anchor["href"])
|
||||||
|
|
||||||
if ["www.youtube.com", "m.youtube.com"].includes?(url.host)
|
if ["www.youtube.com", "m.youtube.com"].includes?(url.host)
|
||||||
if url.path == "/redirect"
|
if url.path == "/redirect"
|
||||||
params = HTTP::Params.parse(url.query.not_nil!)
|
params = HTTP::Params.parse(url.query.not_nil!)
|
||||||
alt_url = params["q"]?
|
anchor["href"] = params["q"]?
|
||||||
alt_url ||= "/"
|
|
||||||
else
|
else
|
||||||
alt_url = url.full_path
|
anchor["href"] = url.full_path
|
||||||
end
|
end
|
||||||
|
|
||||||
alt_link = <<-END_HTML
|
|
||||||
<a href="#{alt_url}">
|
|
||||||
<i class="icon ion-ios-link"></i>
|
|
||||||
</a>
|
|
||||||
END_HTML
|
|
||||||
elsif url.host == "youtu.be"
|
elsif url.host == "youtu.be"
|
||||||
alt_link = <<-END_HTML
|
anchor["href"] = "/watch?v=#{url.path.try &.lchop("/")}&#{url.query}"
|
||||||
<a href="/watch?v=#{url.path.try &.lchop("/")}&#{url.query}">
|
|
||||||
<i class="icon ion-ios-link"></i>
|
|
||||||
</a>
|
|
||||||
END_HTML
|
|
||||||
elsif url.to_s == "#"
|
elsif url.to_s == "#"
|
||||||
begin
|
begin
|
||||||
length_seconds = decode_length_seconds(anchor.content)
|
length_seconds = decode_length_seconds(anchor.content)
|
||||||
@ -228,23 +212,12 @@ def add_alt_links(html)
|
|||||||
length_seconds = decode_time(anchor.content)
|
length_seconds = decode_time(anchor.content)
|
||||||
end
|
end
|
||||||
|
|
||||||
alt_anchor = <<-END_HTML
|
anchor["href"] = "javascript:void(0)"
|
||||||
<a href="javascript:void(0)" onclick="player.currentTime(#{length_seconds})">#{anchor.content}</a>
|
anchor["onclick"] = "player.currentTime(#{length_seconds})"
|
||||||
END_HTML
|
|
||||||
|
|
||||||
html = html.sub(anchor.to_s, alt_anchor)
|
|
||||||
next
|
|
||||||
else
|
|
||||||
alt_link = ""
|
|
||||||
end
|
end
|
||||||
|
|
||||||
alt_links << {anchor.to_s, alt_link}
|
|
||||||
end
|
|
||||||
|
|
||||||
alt_links.each do |original, alternate|
|
|
||||||
html = html.sub(original, original + alternate)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
html = html.to_xml(options: XML::SaveOptions::NO_DECL)
|
||||||
return html
|
return html
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -262,10 +235,10 @@ def fill_links(html, scheme, host)
|
|||||||
end
|
end
|
||||||
|
|
||||||
if host == "www.youtube.com"
|
if host == "www.youtube.com"
|
||||||
html = html.xpath_node(%q(//p[@id="eow-description"])).not_nil!.to_xml
|
html = html.xpath_node(%q(//body)).not_nil!.to_xml
|
||||||
else
|
else
|
||||||
html = html.to_xml(options: XML::SaveOptions::NO_DECL)
|
html = html.to_xml(options: XML::SaveOptions::NO_DECL)
|
||||||
end
|
end
|
||||||
|
|
||||||
html
|
return html
|
||||||
end
|
end
|
||||||
|
@ -116,114 +116,6 @@ def login_req(login_form, f_req)
|
|||||||
return HTTP::Params.encode(data)
|
return HTTP::Params.encode(data)
|
||||||
end
|
end
|
||||||
|
|
||||||
def produce_playlist_url(ucid, index)
|
|
||||||
ucid = ucid.lchop("UC")
|
|
||||||
ucid = "VLUU" + ucid
|
|
||||||
|
|
||||||
continuation = write_var_int(index)
|
|
||||||
continuation.unshift(0x08_u8)
|
|
||||||
slice = continuation.to_unsafe.to_slice(continuation.size)
|
|
||||||
|
|
||||||
continuation = Base64.urlsafe_encode(slice, false)
|
|
||||||
continuation = "PT:" + continuation
|
|
||||||
continuation = continuation.bytes
|
|
||||||
continuation.unshift(0x7a_u8, continuation.size.to_u8)
|
|
||||||
|
|
||||||
slice = continuation.to_unsafe.to_slice(continuation.size)
|
|
||||||
continuation = Base64.urlsafe_encode(slice)
|
|
||||||
continuation = URI.escape(continuation)
|
|
||||||
continuation = continuation.bytes
|
|
||||||
continuation.unshift(continuation.size.to_u8)
|
|
||||||
|
|
||||||
continuation.unshift(ucid.size.to_u8)
|
|
||||||
continuation = ucid.bytes + continuation
|
|
||||||
continuation.unshift(0x12.to_u8, ucid.size.to_u8)
|
|
||||||
continuation.unshift(0xe2_u8, 0xa9_u8, 0x85_u8, 0xb2_u8, 2_u8, continuation.size.to_u8)
|
|
||||||
|
|
||||||
slice = continuation.to_unsafe.to_slice(continuation.size)
|
|
||||||
continuation = Base64.urlsafe_encode(slice)
|
|
||||||
continuation = URI.escape(continuation)
|
|
||||||
|
|
||||||
url = "/browse_ajax?action_continuation=1&continuation=#{continuation}"
|
|
||||||
|
|
||||||
return url
|
|
||||||
end
|
|
||||||
|
|
||||||
def produce_videos_url(ucid, page = 1)
|
|
||||||
page = "#{page}"
|
|
||||||
|
|
||||||
meta = "\x12\x06videos \x00\x30\x02\x38\x01\x60\x01\x6a\x00\x7a"
|
|
||||||
meta += page.size.to_u8.unsafe_chr
|
|
||||||
meta += page
|
|
||||||
meta += "\xb8\x01\x00"
|
|
||||||
|
|
||||||
meta = Base64.urlsafe_encode(meta)
|
|
||||||
meta = URI.escape(meta)
|
|
||||||
|
|
||||||
continuation = "\x12"
|
|
||||||
continuation += ucid.size.to_u8.unsafe_chr
|
|
||||||
continuation += ucid
|
|
||||||
continuation += "\x1a"
|
|
||||||
continuation += meta.size.to_u8.unsafe_chr
|
|
||||||
continuation += meta
|
|
||||||
|
|
||||||
continuation = continuation.size.to_u8.unsafe_chr + continuation
|
|
||||||
continuation = "\xe2\xa9\x85\xb2\x02" + continuation
|
|
||||||
|
|
||||||
continuation = Base64.urlsafe_encode(continuation)
|
|
||||||
continuation = URI.escape(continuation)
|
|
||||||
|
|
||||||
url = "/browse_ajax?continuation=#{continuation}"
|
|
||||||
|
|
||||||
return url
|
|
||||||
end
|
|
||||||
|
|
||||||
def read_var_int(bytes)
|
|
||||||
numRead = 0
|
|
||||||
result = 0
|
|
||||||
|
|
||||||
read = bytes[numRead]
|
|
||||||
|
|
||||||
if bytes.size == 1
|
|
||||||
result = bytes[0].to_i32
|
|
||||||
else
|
|
||||||
while ((read & 0b10000000) != 0)
|
|
||||||
read = bytes[numRead].to_u64
|
|
||||||
value = (read & 0b01111111)
|
|
||||||
result |= (value << (7 * numRead))
|
|
||||||
|
|
||||||
numRead += 1
|
|
||||||
if numRead > 5
|
|
||||||
raise "VarInt is too big"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return result
|
|
||||||
end
|
|
||||||
|
|
||||||
def write_var_int(value : Int)
|
|
||||||
bytes = [] of UInt8
|
|
||||||
value = value.to_u32
|
|
||||||
|
|
||||||
if value == 0
|
|
||||||
bytes = [0_u8]
|
|
||||||
else
|
|
||||||
while value != 0
|
|
||||||
temp = (value & 0b01111111).to_u8
|
|
||||||
value = value >> 7
|
|
||||||
|
|
||||||
if value != 0
|
|
||||||
temp |= 0b10000000
|
|
||||||
end
|
|
||||||
|
|
||||||
bytes << temp
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return bytes
|
|
||||||
end
|
|
||||||
|
|
||||||
def generate_captcha(key)
|
def generate_captcha(key)
|
||||||
minute = Random::Secure.rand(12)
|
minute = Random::Secure.rand(12)
|
||||||
minute_angle = minute * 30
|
minute_angle = minute * 30
|
||||||
@ -268,12 +160,12 @@ def generate_captcha(key)
|
|||||||
|
|
||||||
answer = "#{hour}:#{minute.to_s.rjust(2, '0')}"
|
answer = "#{hour}:#{minute.to_s.rjust(2, '0')}"
|
||||||
token = OpenSSL::HMAC.digest(:sha256, key, answer)
|
token = OpenSSL::HMAC.digest(:sha256, key, answer)
|
||||||
token = Base64.encode(token)
|
token = Base64.urlsafe_encode(token)
|
||||||
|
|
||||||
return {challenge: challenge, token: token}
|
return {challenge: challenge, token: token}
|
||||||
end
|
end
|
||||||
|
|
||||||
def html_to_description(description_html)
|
def html_to_content(description_html)
|
||||||
if !description_html
|
if !description_html
|
||||||
description = ""
|
description = ""
|
||||||
description_html = ""
|
description_html = ""
|
||||||
@ -284,7 +176,7 @@ def html_to_description(description_html)
|
|||||||
description = XML.parse_html(description).content.strip("\n ")
|
description = XML.parse_html(description).content.strip("\n ")
|
||||||
end
|
end
|
||||||
|
|
||||||
return description, description_html
|
return description_html, description
|
||||||
end
|
end
|
||||||
|
|
||||||
def extract_videos(nodeset, ucid = nil)
|
def extract_videos(nodeset, ucid = nil)
|
||||||
@ -301,6 +193,15 @@ def extract_videos(nodeset, ucid = nil)
|
|||||||
next
|
next
|
||||||
end
|
end
|
||||||
|
|
||||||
|
case node.xpath_node(%q(.//div)).not_nil!["class"]
|
||||||
|
when .includes? "yt-lockup-movie-vertical-poster"
|
||||||
|
next
|
||||||
|
when .includes? "yt-lockup-playlist"
|
||||||
|
next
|
||||||
|
when .includes? "yt-lockup-channel"
|
||||||
|
next
|
||||||
|
end
|
||||||
|
|
||||||
title = anchor.content.strip
|
title = anchor.content.strip
|
||||||
id = anchor["href"].lchop("/watch?v=")
|
id = anchor["href"].lchop("/watch?v=")
|
||||||
|
|
||||||
@ -317,40 +218,33 @@ def extract_videos(nodeset, ucid = nil)
|
|||||||
author_id = anchor["href"].split("/")[-1]
|
author_id = anchor["href"].split("/")[-1]
|
||||||
end
|
end
|
||||||
|
|
||||||
# Skip playlists
|
|
||||||
if node.xpath_node(%q(.//div[contains(@class, "yt-playlist-renderer")]))
|
|
||||||
next
|
|
||||||
end
|
|
||||||
|
|
||||||
# Skip movies
|
|
||||||
if node.xpath_node(%q(.//div[contains(@class, "yt-lockup-movie-top-content")]))
|
|
||||||
next
|
|
||||||
end
|
|
||||||
|
|
||||||
metadata = node.xpath_nodes(%q(.//div[contains(@class,"yt-lockup-meta")]/ul/li))
|
metadata = node.xpath_nodes(%q(.//div[contains(@class,"yt-lockup-meta")]/ul/li))
|
||||||
if metadata.size == 0
|
if metadata.empty?
|
||||||
next
|
next
|
||||||
elsif metadata.size == 1
|
|
||||||
if metadata[0].content.starts_with? "Starts"
|
|
||||||
view_count = 0_i64
|
|
||||||
published = Time.epoch(metadata[0].xpath_node(%q(.//span)).not_nil!["data-timestamp"].to_i64)
|
|
||||||
else
|
|
||||||
view_count = metadata[0].content.lchop("Streamed ").split(" ")[0].delete(",").to_i64
|
|
||||||
published = Time.now
|
|
||||||
end
|
|
||||||
else
|
|
||||||
published = decode_date(metadata[0].content)
|
|
||||||
|
|
||||||
view_count = metadata[1].content.split(" ")[0]
|
|
||||||
if view_count == "No"
|
|
||||||
view_count = 0_i64
|
|
||||||
else
|
|
||||||
view_count = view_count.delete(",").to_i64
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
begin
|
||||||
|
published = decode_date(metadata[0].content.lchop("Streamed ").lchop("Starts "))
|
||||||
|
rescue ex
|
||||||
|
end
|
||||||
|
begin
|
||||||
|
published ||= Time.epoch(metadata[0].xpath_node(%q(.//span)).not_nil!["data-timestamp"].to_i64)
|
||||||
|
rescue ex
|
||||||
|
end
|
||||||
|
published ||= Time.now
|
||||||
|
|
||||||
|
begin
|
||||||
|
view_count = metadata[0].content.rchop(" watching").delete(",").try &.to_i64?
|
||||||
|
rescue ex
|
||||||
|
end
|
||||||
|
begin
|
||||||
|
view_count ||= metadata.try &.[1].content.delete("No views,").try &.to_i64?
|
||||||
|
rescue ex
|
||||||
|
end
|
||||||
|
view_count ||= 0_i64
|
||||||
|
|
||||||
description_html = node.xpath_node(%q(.//div[contains(@class, "yt-lockup-description")]))
|
description_html = node.xpath_node(%q(.//div[contains(@class, "yt-lockup-description")]))
|
||||||
description, description_html = html_to_description(description_html)
|
description_html, description = html_to_content(description_html)
|
||||||
|
|
||||||
length_seconds = node.xpath_node(%q(.//span[@class="video-time"]))
|
length_seconds = node.xpath_node(%q(.//span[@class="video-time"]))
|
||||||
if length_seconds
|
if length_seconds
|
||||||
|
@ -9,7 +9,7 @@ macro add_mapping(mapping)
|
|||||||
DB.mapping({{mapping}})
|
DB.mapping({{mapping}})
|
||||||
end
|
end
|
||||||
|
|
||||||
macro templated(filename, template = "layout")
|
macro templated(filename, template = "template")
|
||||||
render "src/invidious/views/#{{{filename}}}.ecr", "src/invidious/views/#{{{template}}}.ecr"
|
render "src/invidious/views/#{{{filename}}}.ecr", "src/invidious/views/#{{{template}}}.ecr"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -31,11 +31,11 @@ class HTTPProxy
|
|||||||
|
|
||||||
if resp[:code]? == 200
|
if resp[:code]? == 200
|
||||||
{% if !flag?(:without_openssl) %}
|
{% if !flag?(:without_openssl) %}
|
||||||
if tls
|
if tls
|
||||||
tls_socket = OpenSSL::SSL::Socket::Client.new(socket, context: tls, sync_close: true, hostname: host)
|
tls_socket = OpenSSL::SSL::Socket::Client.new(socket, context: tls, sync_close: true, hostname: host)
|
||||||
socket = tls_socket
|
socket = tls_socket
|
||||||
end
|
end
|
||||||
{% end %}
|
{% end %}
|
||||||
|
|
||||||
return socket
|
return socket
|
||||||
else
|
else
|
||||||
@ -98,26 +98,44 @@ def get_proxies(country_code = "US")
|
|||||||
headers["Accept"] = "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8"
|
headers["Accept"] = "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8"
|
||||||
headers["Accept-Language"] = "Accept-Language: en-US,en;q=0.9"
|
headers["Accept-Language"] = "Accept-Language: en-US,en;q=0.9"
|
||||||
headers["Host"] = "spys.one"
|
headers["Host"] = "spys.one"
|
||||||
|
headers["Origin"] = "http://spys.one"
|
||||||
|
headers["Referer"] = "http://spys.one/free-proxy-list/#{country_code}/"
|
||||||
headers["Content-Type"] = "application/x-www-form-urlencoded"
|
headers["Content-Type"] = "application/x-www-form-urlencoded"
|
||||||
body = {
|
body = {
|
||||||
"xpp" => "2",
|
"xpp" => "5",
|
||||||
"xf1" => "0",
|
"xf1" => "0",
|
||||||
"xf2" => "2",
|
"xf2" => "0",
|
||||||
"xf4" => "1",
|
"xf4" => "0",
|
||||||
"xf5" => "1",
|
"xf5" => "1",
|
||||||
}
|
}
|
||||||
response = client.post("/free-proxy-list/#{country_code}/", headers, form: body)
|
response = client.post("/free-proxy-list/#{country_code}/", headers, form: body)
|
||||||
response = XML.parse_html(response.body)
|
response = XML.parse_html(response.body)
|
||||||
|
|
||||||
|
mapping = response.xpath_node(%q(.//body/script)).not_nil!.content
|
||||||
|
mapping = mapping.match(/\}\('(?<p>[^']+)',\d+,\d+,'(?<x>[^']+)'/).not_nil!
|
||||||
|
p = mapping["p"].not_nil!
|
||||||
|
x = mapping["x"].not_nil!
|
||||||
|
mapping = decrypt_port(p, x)
|
||||||
|
|
||||||
proxies = [] of {ip: String, port: Int32, score: Float64}
|
proxies = [] of {ip: String, port: Int32, score: Float64}
|
||||||
response = response.xpath_nodes(%q(//table))[1]
|
response = response.xpath_node(%q(//tr/td/table)).not_nil!
|
||||||
response.xpath_nodes(%q(.//tr)).each do |node|
|
response.xpath_nodes(%q(.//tr)).each do |node|
|
||||||
if !node["onmouseover"]?
|
if !node["onmouseover"]?
|
||||||
next
|
next
|
||||||
end
|
end
|
||||||
|
|
||||||
ip = node.xpath_node(%q(.//td[1]/font[2])).to_s.match(/<font class="spy14">(?<address>[^<]+)</).not_nil!["address"]
|
ip = node.xpath_node(%q(.//td[1]/font[2])).to_s.match(/<font class="spy14">(?<address>[^<]+)</).not_nil!["address"]
|
||||||
port = 3128
|
encrypted_port = node.xpath_node(%q(.//td[1]/font[2]/script)).not_nil!.content
|
||||||
|
encrypted_port = encrypted_port.match(/<\\\/font>"\+(?<encrypted_port>[\d\D]+)\)$/).not_nil!["encrypted_port"]
|
||||||
|
|
||||||
|
port = ""
|
||||||
|
encrypted_port.split("+").each do |number|
|
||||||
|
number = number.delete("()")
|
||||||
|
left_side, right_side = number.split("^")
|
||||||
|
result = mapping[left_side] ^ mapping[right_side]
|
||||||
|
port = "#{port}#{result}"
|
||||||
|
end
|
||||||
|
port = port.to_i
|
||||||
|
|
||||||
latency = node.xpath_node(%q(.//td[6])).not_nil!.content.to_f
|
latency = node.xpath_node(%q(.//td[6])).not_nil!.content.to_f
|
||||||
speed = node.xpath_node(%q(.//td[7]/font/table)).not_nil!["width"].to_f
|
speed = node.xpath_node(%q(.//td[7]/font/table)).not_nil!["width"].to_f
|
||||||
@ -141,3 +159,52 @@ def get_proxies(country_code = "US")
|
|||||||
|
|
||||||
return proxies
|
return proxies
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def decrypt_port(p, x)
|
||||||
|
x = x.split("^")
|
||||||
|
s = {} of String => String
|
||||||
|
|
||||||
|
60.times do |i|
|
||||||
|
if x[i]?.try &.empty?
|
||||||
|
s[y_func(i)] = y_func(i)
|
||||||
|
else
|
||||||
|
s[y_func(i)] = x[i]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
x = s
|
||||||
|
p = p.gsub(/\b\w+\b/, x)
|
||||||
|
|
||||||
|
p = p.split(";")
|
||||||
|
p = p.map { |item| item.split("=") }
|
||||||
|
|
||||||
|
mapping = {} of String => Int32
|
||||||
|
p.each do |item|
|
||||||
|
if item == [""]
|
||||||
|
next
|
||||||
|
end
|
||||||
|
|
||||||
|
key = item[0]
|
||||||
|
value = item[1]
|
||||||
|
value = value.split("^")
|
||||||
|
|
||||||
|
if value.size == 1
|
||||||
|
value = value[0].to_i
|
||||||
|
else
|
||||||
|
left_side = value[0].to_i?
|
||||||
|
left_side ||= mapping[value[0]]
|
||||||
|
right_side = value[1].to_i?
|
||||||
|
right_side ||= mapping[value[1]]
|
||||||
|
|
||||||
|
value = left_side ^ right_side
|
||||||
|
end
|
||||||
|
|
||||||
|
mapping[key] = value
|
||||||
|
end
|
||||||
|
|
||||||
|
return mapping
|
||||||
|
end
|
||||||
|
|
||||||
|
def y_func(c)
|
||||||
|
return (c < 60 ? "" : y_func((c/60).to_i)) + ((c = c % 60) > 35 ? ((c.to_u8 + 29).unsafe_chr) : c.to_s(36))
|
||||||
|
end
|
||||||
|
@ -64,10 +64,23 @@ end
|
|||||||
|
|
||||||
def decode_date(string : String)
|
def decode_date(string : String)
|
||||||
# String matches 'YYYY'
|
# String matches 'YYYY'
|
||||||
if string.match(/\d{4}/)
|
if string.match(/^\d{4}/)
|
||||||
return Time.new(string.to_i, 1, 1)
|
return Time.new(string.to_i, 1, 1)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Try to parse as format Jul 10, 2000
|
||||||
|
begin
|
||||||
|
return Time.parse(string, "%b %-d, %Y", Time::Location.local)
|
||||||
|
rescue ex
|
||||||
|
end
|
||||||
|
|
||||||
|
case string
|
||||||
|
when "today"
|
||||||
|
return Time.now
|
||||||
|
when "yesterday"
|
||||||
|
return Time.now - 1.day
|
||||||
|
end
|
||||||
|
|
||||||
# String matches format "20 hours ago", "4 months ago"...
|
# String matches format "20 hours ago", "4 months ago"...
|
||||||
date = string.split(" ")[-3, 3]
|
date = string.split(" ")[-3, 3]
|
||||||
delta = date[0].to_i
|
delta = date[0].to_i
|
||||||
@ -150,10 +163,27 @@ def make_host_url(ssl, host)
|
|||||||
end
|
end
|
||||||
|
|
||||||
def get_referer(env, fallback = "/")
|
def get_referer(env, fallback = "/")
|
||||||
referer = env.request.headers["referer"]?
|
referer = env.params.query["referer"]?
|
||||||
|
referer ||= env.request.headers["referer"]?
|
||||||
referer ||= fallback
|
referer ||= fallback
|
||||||
|
|
||||||
referer = URI.parse(referer).full_path
|
referer = URI.parse(referer)
|
||||||
|
|
||||||
|
# "Unroll" nested referers
|
||||||
|
loop do
|
||||||
|
if referer.query
|
||||||
|
params = HTTP::Params.parse(referer.query.not_nil!)
|
||||||
|
if params["referer"]?
|
||||||
|
referer = URI.parse(URI.unescape(params["referer"]))
|
||||||
|
else
|
||||||
|
break
|
||||||
|
end
|
||||||
|
else
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
referer = referer.full_path
|
||||||
|
|
||||||
if referer == env.request.path
|
if referer == env.request.path
|
||||||
referer = fallback
|
referer = fallback
|
||||||
@ -161,3 +191,49 @@ def get_referer(env, fallback = "/")
|
|||||||
|
|
||||||
return referer
|
return referer
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def read_var_int(bytes)
|
||||||
|
numRead = 0
|
||||||
|
result = 0
|
||||||
|
|
||||||
|
read = bytes[numRead]
|
||||||
|
|
||||||
|
if bytes.size == 1
|
||||||
|
result = bytes[0].to_i32
|
||||||
|
else
|
||||||
|
while ((read & 0b10000000) != 0)
|
||||||
|
read = bytes[numRead].to_u64
|
||||||
|
value = (read & 0b01111111)
|
||||||
|
result |= (value << (7 * numRead))
|
||||||
|
|
||||||
|
numRead += 1
|
||||||
|
if numRead > 5
|
||||||
|
raise "VarInt is too big"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
def write_var_int(value : Int)
|
||||||
|
bytes = [] of UInt8
|
||||||
|
value = value.to_u32
|
||||||
|
|
||||||
|
if value == 0
|
||||||
|
bytes = [0_u8]
|
||||||
|
else
|
||||||
|
while value != 0
|
||||||
|
temp = (value & 0b01111111).to_u8
|
||||||
|
value = value >> 7
|
||||||
|
|
||||||
|
if value != 0
|
||||||
|
temp |= 0b10000000
|
||||||
|
end
|
||||||
|
|
||||||
|
bytes << temp
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return bytes
|
||||||
|
end
|
||||||
|
@ -2,13 +2,13 @@ def crawl_videos(db)
|
|||||||
ids = Deque(String).new
|
ids = Deque(String).new
|
||||||
random = Random.new
|
random = Random.new
|
||||||
|
|
||||||
search(random.base64(3)).each do |video|
|
search(random.base64(3)).as(Tuple)[1].each do |video|
|
||||||
ids << video.id
|
ids << video.id
|
||||||
end
|
end
|
||||||
|
|
||||||
loop do
|
loop do
|
||||||
if ids.empty?
|
if ids.empty?
|
||||||
search(random.base64(3)).each do |video|
|
search(random.base64(3)).as(Tuple)[1].each do |video|
|
||||||
ids << video.id
|
ids << video.id
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
153
src/invidious/playlists.cr
Normal file
153
src/invidious/playlists.cr
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
class Playlist
|
||||||
|
add_mapping({
|
||||||
|
title: String,
|
||||||
|
id: String,
|
||||||
|
author: String,
|
||||||
|
ucid: String,
|
||||||
|
description: String,
|
||||||
|
description_html: String,
|
||||||
|
video_count: Int32,
|
||||||
|
views: Int64,
|
||||||
|
updated: Time,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
class PlaylistVideo
|
||||||
|
add_mapping({
|
||||||
|
title: String,
|
||||||
|
id: String,
|
||||||
|
author: String,
|
||||||
|
ucid: String,
|
||||||
|
length_seconds: Int32,
|
||||||
|
published: Time,
|
||||||
|
playlists: Array(String),
|
||||||
|
index: Int32,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
def extract_playlist(plid, page)
|
||||||
|
index = (page - 1) * 100
|
||||||
|
url = produce_playlist_url(plid, index)
|
||||||
|
|
||||||
|
client = make_client(YT_URL)
|
||||||
|
response = client.get(url)
|
||||||
|
response = JSON.parse(response.body)
|
||||||
|
if !response["content_html"]? || response["content_html"].as_s.empty?
|
||||||
|
raise "Playlist does not exist"
|
||||||
|
end
|
||||||
|
|
||||||
|
videos = [] of PlaylistVideo
|
||||||
|
|
||||||
|
document = XML.parse_html(response["content_html"].as_s)
|
||||||
|
anchor = document.xpath_node(%q(//div[@class="pl-video-owner"]/a))
|
||||||
|
if anchor
|
||||||
|
document.xpath_nodes(%q(.//tr[contains(@class, "pl-video")])).each_with_index do |video, offset|
|
||||||
|
anchor = video.xpath_node(%q(.//td[@class="pl-video-title"]))
|
||||||
|
if !anchor
|
||||||
|
next
|
||||||
|
end
|
||||||
|
|
||||||
|
title = anchor.xpath_node(%q(.//a)).not_nil!.content.strip(" \n")
|
||||||
|
id = anchor.xpath_node(%q(.//a)).not_nil!["href"].lchop("/watch?v=")[0, 11]
|
||||||
|
|
||||||
|
anchor = anchor.xpath_node(%q(.//div[@class="pl-video-owner"]/a))
|
||||||
|
if anchor
|
||||||
|
author = anchor.content
|
||||||
|
ucid = anchor["href"].split("/")[2]
|
||||||
|
else
|
||||||
|
author = ""
|
||||||
|
ucid = ""
|
||||||
|
end
|
||||||
|
|
||||||
|
anchor = video.xpath_node(%q(.//td[@class="pl-video-time"]/div/div[1]))
|
||||||
|
if anchor && !anchor.content.empty?
|
||||||
|
length_seconds = decode_length_seconds(anchor.content)
|
||||||
|
else
|
||||||
|
length_seconds = 0
|
||||||
|
end
|
||||||
|
|
||||||
|
videos << PlaylistVideo.new(
|
||||||
|
title,
|
||||||
|
id,
|
||||||
|
author,
|
||||||
|
ucid,
|
||||||
|
length_seconds,
|
||||||
|
Time.now,
|
||||||
|
[plid],
|
||||||
|
index + offset,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return videos
|
||||||
|
end
|
||||||
|
|
||||||
|
def produce_playlist_url(id, index)
|
||||||
|
if id.starts_with? "UC"
|
||||||
|
id = "UU" + id.lchop("UC")
|
||||||
|
end
|
||||||
|
ucid = "VL" + id
|
||||||
|
|
||||||
|
continuation = [0x08_u8] + write_var_int(index)
|
||||||
|
slice = continuation.to_unsafe.to_slice(continuation.size)
|
||||||
|
slice = Base64.urlsafe_encode(slice, false)
|
||||||
|
|
||||||
|
# Inner Base64
|
||||||
|
continuation = "PT:" + slice
|
||||||
|
continuation = [0x7a_u8, continuation.bytes.size.to_u8] + continuation.bytes
|
||||||
|
slice = continuation.to_unsafe.to_slice(continuation.size)
|
||||||
|
slice = Base64.urlsafe_encode(slice)
|
||||||
|
slice = URI.escape(slice)
|
||||||
|
|
||||||
|
# Outer Base64
|
||||||
|
continuation = [0x1a_u8, slice.bytes.size.to_u8] + slice.bytes
|
||||||
|
continuation = ucid.bytes + continuation
|
||||||
|
continuation = [0x12_u8, ucid.size.to_u8] + continuation
|
||||||
|
continuation = [0xe2_u8, 0xa9_u8, 0x85_u8, 0xb2_u8, 2_u8, continuation.size.to_u8] + continuation
|
||||||
|
|
||||||
|
# Wrap bytes
|
||||||
|
slice = continuation.to_unsafe.to_slice(continuation.size)
|
||||||
|
slice = Base64.urlsafe_encode(slice)
|
||||||
|
slice = URI.escape(slice)
|
||||||
|
continuation = slice
|
||||||
|
|
||||||
|
url = "/browse_ajax?action_continuation=1&continuation=#{continuation}"
|
||||||
|
|
||||||
|
return url
|
||||||
|
end
|
||||||
|
|
||||||
|
def fetch_playlist(plid)
|
||||||
|
client = make_client(YT_URL)
|
||||||
|
response = client.get("/playlist?list=#{plid}&disable_polymer=1")
|
||||||
|
document = XML.parse_html(response.body)
|
||||||
|
|
||||||
|
title = document.xpath_node(%q(//h1[@class="pl-header-title"])).not_nil!.content
|
||||||
|
title = title.strip(" \n")
|
||||||
|
|
||||||
|
description_html = document.xpath_node(%q(//span[@class="pl-header-description-text"]/div/div[1]))
|
||||||
|
description, description_html = html_to_content(description_html)
|
||||||
|
|
||||||
|
anchor = document.xpath_node(%q(//ul[@class="pl-header-details"])).not_nil!
|
||||||
|
author = anchor.xpath_node(%q(.//li[1]/a)).not_nil!.content
|
||||||
|
ucid = anchor.xpath_node(%q(.//li[1]/a)).not_nil!["href"].split("/")[2]
|
||||||
|
|
||||||
|
video_count = anchor.xpath_node(%q(.//li[2])).not_nil!.content.delete("videos, ").to_i
|
||||||
|
views = anchor.xpath_node(%q(.//li[3])).not_nil!.content.delete("views, ").to_i64
|
||||||
|
|
||||||
|
updated = anchor.xpath_node(%q(.//li[4])).not_nil!.content.lchop("Last updated on ").lchop("Updated ")
|
||||||
|
updated = decode_date(updated)
|
||||||
|
|
||||||
|
playlist = Playlist.new(
|
||||||
|
title,
|
||||||
|
plid,
|
||||||
|
author,
|
||||||
|
ucid,
|
||||||
|
description,
|
||||||
|
description_html,
|
||||||
|
video_count,
|
||||||
|
views,
|
||||||
|
updated
|
||||||
|
)
|
||||||
|
|
||||||
|
return playlist
|
||||||
|
end
|
@ -14,31 +14,36 @@ end
|
|||||||
|
|
||||||
def search(query, page = 1, search_params = build_search_params(content_type: "video"))
|
def search(query, page = 1, search_params = build_search_params(content_type: "video"))
|
||||||
client = make_client(YT_URL)
|
client = make_client(YT_URL)
|
||||||
|
if query.empty?
|
||||||
|
return {0, [] of SearchVideo}
|
||||||
|
end
|
||||||
|
|
||||||
html = client.get("/results?q=#{URI.escape(query)}&page=#{page}&sp=#{search_params}&disable_polymer=1").body
|
html = client.get("/results?q=#{URI.escape(query)}&page=#{page}&sp=#{search_params}&disable_polymer=1").body
|
||||||
if html.empty?
|
if html.empty?
|
||||||
return [] of SearchVideo
|
return {0, [] of SearchVideo}
|
||||||
end
|
end
|
||||||
|
|
||||||
html = XML.parse_html(html)
|
html = XML.parse_html(html)
|
||||||
nodeset = html.xpath_nodes(%q(//ol[@class="item-section"]/li))
|
nodeset = html.xpath_nodes(%q(//ol[@class="item-section"]/li))
|
||||||
videos = extract_videos(nodeset)
|
videos = extract_videos(nodeset)
|
||||||
|
|
||||||
return videos
|
return {nodeset.size, videos}
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_search_params(sort_by = "relevance", date : String = "", content_type : String = "", duration : String = "", features : Array(String) = [] of String)
|
def build_search_params(sort : String = "relevance", date : String = "", content_type : String = "",
|
||||||
|
duration : String = "", features : Array(String) = [] of String)
|
||||||
head = "\x08"
|
head = "\x08"
|
||||||
head += case sort_by
|
head += case sort
|
||||||
when "relevance"
|
when "relevance"
|
||||||
"\x00"
|
"\x00"
|
||||||
when "rating"
|
when "rating"
|
||||||
"\x01"
|
"\x01"
|
||||||
when "upload_date"
|
when "upload_date", "date"
|
||||||
"\x02"
|
"\x02"
|
||||||
when "view_count"
|
when "view_count", "views"
|
||||||
"\x03"
|
"\x03"
|
||||||
else
|
else
|
||||||
raise "No sort #{sort_by}"
|
raise "No sort #{sort}"
|
||||||
end
|
end
|
||||||
|
|
||||||
body = ""
|
body = ""
|
||||||
@ -87,7 +92,7 @@ def build_search_params(sort_by = "relevance", date : String = "", content_type
|
|||||||
"\x20\x01"
|
"\x20\x01"
|
||||||
when "subtitles"
|
when "subtitles"
|
||||||
"\x28\x01"
|
"\x28\x01"
|
||||||
when "creative_commons"
|
when "creative_commons", "cc"
|
||||||
"\x30\x01"
|
"\x30\x01"
|
||||||
when "3d"
|
when "3d"
|
||||||
"\x38\x01"
|
"\x38\x01"
|
||||||
|
@ -3,23 +3,22 @@ def fetch_decrypt_function(client, id = "CvFH_6DNRCY")
|
|||||||
url = document.match(/src="(?<url>\/yts\/jsbin\/player-.{9}\/en_US\/base.js)"/).not_nil!["url"]
|
url = document.match(/src="(?<url>\/yts\/jsbin\/player-.{9}\/en_US\/base.js)"/).not_nil!["url"]
|
||||||
player = client.get(url).body
|
player = client.get(url).body
|
||||||
|
|
||||||
function_name = player.match(/\(b\|\|\(b="signature"\),d.set\(b,(?<name>[a-zA-Z0-9]{2})\(c\)\)\)/).not_nil!["name"]
|
function_name = player.match(/"signature",(?<name>[a-zA-Z0-9]{2})\(/).not_nil!["name"]
|
||||||
function_body = player.match(/#{function_name}=function\(a\){(?<body>[^}]+)}/).not_nil!["body"]
|
function_body = player.match(/^#{function_name}=function\(a\){(?<body>[^}]+)}/m).not_nil!["body"]
|
||||||
function_body = function_body.split(";")[1..-2]
|
function_body = function_body.split(";")[1..-2]
|
||||||
|
|
||||||
var_name = function_body[0][0, 2]
|
var_name = function_body[0][0, 2]
|
||||||
|
var_body = player.delete("\n").match(/var #{var_name}={(?<body>(.*?))};/).not_nil!["body"]
|
||||||
|
|
||||||
operations = {} of String => String
|
operations = {} of String => String
|
||||||
matches = player.delete("\n").match(/var #{var_name}={(?<op1>[a-zA-Z0-9]{2}:[^}]+}),(?<op2>[a-zA-Z0-9]{2}:[^}]+}),(?<op3>[a-zA-Z0-9]{2}:[^}]+})};/).not_nil!
|
var_body.split("},").each do |operation|
|
||||||
3.times do |i|
|
op_name = operation.match(/^[^:]+/).not_nil![0]
|
||||||
operation = matches["op#{i + 1}"]
|
op_body = operation.match(/\{[^}]+/).not_nil![0]
|
||||||
op_name = operation[0, 2]
|
|
||||||
|
|
||||||
op_body = operation.match(/\{[^}]+\}/).not_nil![0]
|
|
||||||
case op_body
|
case op_body
|
||||||
when "{a.reverse()}"
|
when "{a.reverse()"
|
||||||
operations[op_name] = "a"
|
operations[op_name] = "a"
|
||||||
when "{a.splice(0,b)}"
|
when "{a.splice(0,b)"
|
||||||
operations[op_name] = "b"
|
operations[op_name] = "b"
|
||||||
else
|
else
|
||||||
operations[op_name] = "c"
|
operations[op_name] = "c"
|
||||||
@ -28,11 +27,10 @@ def fetch_decrypt_function(client, id = "CvFH_6DNRCY")
|
|||||||
|
|
||||||
decrypt_function = [] of {name: String, value: Int32}
|
decrypt_function = [] of {name: String, value: Int32}
|
||||||
function_body.each do |function|
|
function_body.each do |function|
|
||||||
function = function.lchop(var_name + ".")
|
function = function.lchop(var_name).delete("[].")
|
||||||
op_name = function[0, 2]
|
|
||||||
|
|
||||||
function = function.lchop(op_name + "(a,")
|
op_name = function.match(/[^\(]+/).not_nil![0]
|
||||||
value = function.rchop(")").to_i
|
value = function.match(/\(a,(?<value>[\d]+)\)/).not_nil!["value"].to_i
|
||||||
|
|
||||||
decrypt_function << {name: operations[op_name], value: value}
|
decrypt_function << {name: operations[op_name], value: value}
|
||||||
end
|
end
|
||||||
|
@ -10,7 +10,7 @@ class User
|
|||||||
end
|
end
|
||||||
|
|
||||||
add_mapping({
|
add_mapping({
|
||||||
id: String,
|
id: Array(String),
|
||||||
updated: Time,
|
updated: Time,
|
||||||
notifications: Array(String),
|
notifications: Array(String),
|
||||||
subscriptions: Array(String),
|
subscriptions: Array(String),
|
||||||
@ -27,22 +27,46 @@ class User
|
|||||||
end
|
end
|
||||||
|
|
||||||
DEFAULT_USER_PREFERENCES = Preferences.from_json({
|
DEFAULT_USER_PREFERENCES = Preferences.from_json({
|
||||||
"video_loop" => false,
|
"video_loop" => false,
|
||||||
"autoplay" => false,
|
"autoplay" => false,
|
||||||
"speed" => 1.0,
|
"speed" => 1.0,
|
||||||
"quality" => "hd720",
|
"quality" => "hd720",
|
||||||
"volume" => 100,
|
"volume" => 100,
|
||||||
"comments" => "youtube",
|
"comments" => ["youtube", ""],
|
||||||
"captions" => ["", "", ""],
|
"captions" => ["", "", ""],
|
||||||
"dark_mode" => false,
|
"related_videos" => true,
|
||||||
"thin_mode " => false,
|
"dark_mode" => false,
|
||||||
"max_results" => 40,
|
"thin_mode " => false,
|
||||||
"sort" => "published",
|
"max_results" => 40,
|
||||||
"latest_only" => false,
|
"sort" => "published",
|
||||||
"unseen_only" => false,
|
"latest_only" => false,
|
||||||
|
"unseen_only" => false,
|
||||||
}.to_json)
|
}.to_json)
|
||||||
|
|
||||||
class Preferences
|
class Preferences
|
||||||
|
module StringToArray
|
||||||
|
def self.to_json(value : Array(String), json : JSON::Builder)
|
||||||
|
json.array do
|
||||||
|
value.each do |element|
|
||||||
|
json.string element
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.from_json(value : JSON::PullParser) : Array(String)
|
||||||
|
begin
|
||||||
|
result = [] of String
|
||||||
|
value.read_array do
|
||||||
|
result << value.read_string
|
||||||
|
end
|
||||||
|
rescue ex
|
||||||
|
result = [value.read_string, ""]
|
||||||
|
end
|
||||||
|
|
||||||
|
result
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
JSON.mapping({
|
JSON.mapping({
|
||||||
video_loop: Bool,
|
video_loop: Bool,
|
||||||
autoplay: Bool,
|
autoplay: Bool,
|
||||||
@ -50,8 +74,9 @@ class Preferences
|
|||||||
quality: String,
|
quality: String,
|
||||||
volume: Int32,
|
volume: Int32,
|
||||||
comments: {
|
comments: {
|
||||||
type: String,
|
type: Array(String),
|
||||||
default: "youtube",
|
default: ["youtube", ""],
|
||||||
|
converter: StringToArray,
|
||||||
},
|
},
|
||||||
captions: {
|
captions: {
|
||||||
type: Array(String),
|
type: Array(String),
|
||||||
@ -61,6 +86,10 @@ class Preferences
|
|||||||
type: Bool,
|
type: Bool,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
related_videos: {
|
||||||
|
type: Bool,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
dark_mode: Bool,
|
dark_mode: Bool,
|
||||||
thin_mode: {
|
thin_mode: {
|
||||||
type: Bool,
|
type: Bool,
|
||||||
@ -78,8 +107,8 @@ class Preferences
|
|||||||
end
|
end
|
||||||
|
|
||||||
def get_user(sid, client, headers, db, refresh = true)
|
def get_user(sid, client, headers, db, refresh = true)
|
||||||
if db.query_one?("SELECT EXISTS (SELECT true FROM users WHERE id = $1)", sid, as: Bool)
|
if db.query_one?("SELECT EXISTS (SELECT true FROM users WHERE $1 = ANY(id))", sid, as: Bool)
|
||||||
user = db.query_one("SELECT * FROM users WHERE id = $1", sid, as: User)
|
user = db.query_one("SELECT * FROM users WHERE $1 = ANY(id)", sid, as: User)
|
||||||
|
|
||||||
if refresh && Time.now - user.updated > 1.minute
|
if refresh && Time.now - user.updated > 1.minute
|
||||||
user = fetch_user(sid, client, headers, db)
|
user = fetch_user(sid, client, headers, db)
|
||||||
@ -89,7 +118,7 @@ def get_user(sid, client, headers, db, refresh = true)
|
|||||||
args = arg_array(user_array)
|
args = arg_array(user_array)
|
||||||
|
|
||||||
db.exec("INSERT INTO users VALUES (#{args}) \
|
db.exec("INSERT INTO users VALUES (#{args}) \
|
||||||
ON CONFLICT (email) DO UPDATE SET id = $1, updated = $2, subscriptions = $4", user_array)
|
ON CONFLICT (email) DO UPDATE SET id = users.id || $1, updated = $2, subscriptions = $4", user_array)
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
user = fetch_user(sid, client, headers, db)
|
user = fetch_user(sid, client, headers, db)
|
||||||
@ -99,7 +128,7 @@ def get_user(sid, client, headers, db, refresh = true)
|
|||||||
args = arg_array(user.to_a)
|
args = arg_array(user.to_a)
|
||||||
|
|
||||||
db.exec("INSERT INTO users VALUES (#{args}) \
|
db.exec("INSERT INTO users VALUES (#{args}) \
|
||||||
ON CONFLICT (email) DO UPDATE SET id = $1, updated = $2, subscriptions = $4", user_array)
|
ON CONFLICT (email) DO UPDATE SET id = users.id || $1, updated = $2, subscriptions = $4", user_array)
|
||||||
end
|
end
|
||||||
|
|
||||||
return user
|
return user
|
||||||
@ -132,7 +161,7 @@ def fetch_user(sid, client, headers, db)
|
|||||||
|
|
||||||
token = Base64.urlsafe_encode(Random::Secure.random_bytes(32))
|
token = Base64.urlsafe_encode(Random::Secure.random_bytes(32))
|
||||||
|
|
||||||
user = User.new(sid, Time.now, [] of String, channels, email, DEFAULT_USER_PREFERENCES, nil, token, [] of String)
|
user = User.new([sid], Time.now, [] of String, channels, email, DEFAULT_USER_PREFERENCES, nil, token, [] of String)
|
||||||
return user
|
return user
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -140,7 +169,7 @@ def create_user(sid, email, password)
|
|||||||
password = Crypto::Bcrypt::Password.create(password, cost: 10)
|
password = Crypto::Bcrypt::Password.create(password, cost: 10)
|
||||||
token = Base64.urlsafe_encode(Random::Secure.random_bytes(32))
|
token = Base64.urlsafe_encode(Random::Secure.random_bytes(32))
|
||||||
|
|
||||||
user = User.new(sid, Time.now, [] of String, [] of String, email, DEFAULT_USER_PREFERENCES, password.to_s, token, [] of String)
|
user = User.new([sid], Time.now, [] of String, [] of String, email, DEFAULT_USER_PREFERENCES, password.to_s, token, [] of String)
|
||||||
|
|
||||||
return user
|
return user
|
||||||
end
|
end
|
||||||
|
@ -228,6 +228,8 @@ VIDEO_FORMATS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class Video
|
class Video
|
||||||
|
property player_json : JSON::Any?
|
||||||
|
|
||||||
module HTTPParamConverter
|
module HTTPParamConverter
|
||||||
def self.from_rs(rs)
|
def self.from_rs(rs)
|
||||||
HTTP::Params.parse(rs.read(String))
|
HTTP::Params.parse(rs.read(String))
|
||||||
@ -287,9 +289,15 @@ class Video
|
|||||||
return audio_streams
|
return audio_streams
|
||||||
end
|
end
|
||||||
|
|
||||||
def captions
|
def player_response
|
||||||
player_response = JSON.parse(self.info["player_response"])
|
if !@player_json
|
||||||
|
@player_json = JSON.parse(@info["player_response"])
|
||||||
|
end
|
||||||
|
|
||||||
|
return @player_json.not_nil!
|
||||||
|
end
|
||||||
|
|
||||||
|
def captions
|
||||||
captions = [] of Caption
|
captions = [] of Caption
|
||||||
if player_response["captions"]?
|
if player_response["captions"]?
|
||||||
caption_list = player_response["captions"]["playerCaptionsTracklistRenderer"]["captionTracks"]?.try &.as_a
|
caption_list = player_response["captions"]["playerCaptionsTracklistRenderer"]["captionTracks"]?.try &.as_a
|
||||||
@ -337,6 +345,10 @@ class Video
|
|||||||
allowed_regions: Array(String),
|
allowed_regions: Array(String),
|
||||||
is_family_friendly: Bool,
|
is_family_friendly: Bool,
|
||||||
genre: String,
|
genre: String,
|
||||||
|
genre_url: {
|
||||||
|
type: String,
|
||||||
|
default: "/",
|
||||||
|
},
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -363,10 +375,12 @@ def get_video(id, db, refresh = true)
|
|||||||
begin
|
begin
|
||||||
video = fetch_video(id)
|
video = fetch_video(id)
|
||||||
video_array = video.to_a
|
video_array = video.to_a
|
||||||
|
|
||||||
args = arg_array(video_array[1..-1], 2)
|
args = arg_array(video_array[1..-1], 2)
|
||||||
|
|
||||||
db.exec("UPDATE videos SET (info,updated,title,views,likes,dislikes,wilson_score,\
|
db.exec("UPDATE videos SET (info,updated,title,views,likes,dislikes,wilson_score,\
|
||||||
published,description,language,author,ucid, allowed_regions, is_family_friendly, genre)\
|
published,description,language,author,ucid, allowed_regions, is_family_friendly,\
|
||||||
|
genre, genre_url)\
|
||||||
= (#{args}) WHERE id = $1", video_array)
|
= (#{args}) WHERE id = $1", video_array)
|
||||||
rescue ex
|
rescue ex
|
||||||
db.exec("DELETE FROM videos * WHERE id = $1", id)
|
db.exec("DELETE FROM videos * WHERE id = $1", id)
|
||||||
@ -376,6 +390,7 @@ def get_video(id, db, refresh = true)
|
|||||||
else
|
else
|
||||||
video = fetch_video(id)
|
video = fetch_video(id)
|
||||||
video_array = video.to_a
|
video_array = video.to_a
|
||||||
|
|
||||||
args = arg_array(video_array)
|
args = arg_array(video_array)
|
||||||
|
|
||||||
db.exec("INSERT INTO videos VALUES (#{args}) ON CONFLICT (id) DO NOTHING", video_array)
|
db.exec("INSERT INTO videos VALUES (#{args}) ON CONFLICT (id) DO NOTHING", video_array)
|
||||||
@ -482,10 +497,12 @@ def fetch_video(id)
|
|||||||
|
|
||||||
allowed_regions = html.xpath_node(%q(//meta[@itemprop="regionsAllowed"])).not_nil!["content"].split(",")
|
allowed_regions = html.xpath_node(%q(//meta[@itemprop="regionsAllowed"])).not_nil!["content"].split(",")
|
||||||
is_family_friendly = html.xpath_node(%q(//meta[@itemprop="isFamilyFriendly"])).not_nil!["content"] == "True"
|
is_family_friendly = html.xpath_node(%q(//meta[@itemprop="isFamilyFriendly"])).not_nil!["content"] == "True"
|
||||||
|
|
||||||
genre = html.xpath_node(%q(//meta[@itemprop="genre"])).not_nil!["content"]
|
genre = html.xpath_node(%q(//meta[@itemprop="genre"])).not_nil!["content"]
|
||||||
|
genre_url = html.xpath_node(%(//a[text()="#{genre}"])).not_nil!["href"]
|
||||||
|
|
||||||
video = Video.new(id, info, Time.now, title, views, likes, dislikes, wilson_score, published, description,
|
video = Video.new(id, info, Time.now, title, views, likes, dislikes, wilson_score, published, description,
|
||||||
nil, author, ucid, allowed_regions, is_family_friendly, genre)
|
nil, author, ucid, allowed_regions, is_family_friendly, genre, genre_url)
|
||||||
|
|
||||||
return video
|
return video
|
||||||
end
|
end
|
||||||
@ -496,24 +513,37 @@ end
|
|||||||
|
|
||||||
def process_video_params(query, preferences)
|
def process_video_params(query, preferences)
|
||||||
autoplay = query["autoplay"]?.try &.to_i?
|
autoplay = query["autoplay"]?.try &.to_i?
|
||||||
|
preferred_captions = query["subtitles"]?.try &.split(",").map { |a| a.downcase }
|
||||||
|
quality = query["quality"]?
|
||||||
|
speed = query["speed"]?.try &.to_f?
|
||||||
video_loop = query["loop"]?.try &.to_i?
|
video_loop = query["loop"]?.try &.to_i?
|
||||||
|
volume = query["volume"]?.try &.to_i?
|
||||||
|
|
||||||
if preferences
|
if preferences
|
||||||
autoplay ||= preferences.autoplay.to_unsafe
|
autoplay ||= preferences.autoplay.to_unsafe
|
||||||
|
preferred_captions ||= preferences.captions
|
||||||
|
quality ||= preferences.quality
|
||||||
|
speed ||= preferences.speed
|
||||||
video_loop ||= preferences.video_loop.to_unsafe
|
video_loop ||= preferences.video_loop.to_unsafe
|
||||||
|
volume ||= preferences.volume
|
||||||
end
|
end
|
||||||
autoplay ||= 0
|
|
||||||
autoplay = autoplay == 1
|
|
||||||
|
|
||||||
|
autoplay ||= 0
|
||||||
|
preferred_captions ||= [] of String
|
||||||
|
quality ||= "hd720"
|
||||||
|
speed ||= 1
|
||||||
video_loop ||= 0
|
video_loop ||= 0
|
||||||
|
volume ||= 100
|
||||||
|
|
||||||
|
autoplay = autoplay == 1
|
||||||
video_loop = video_loop == 1
|
video_loop = video_loop == 1
|
||||||
|
|
||||||
if query["t"]?
|
if query["t"]?
|
||||||
video_start = decode_time(query["t"])
|
video_start = decode_time(query["t"])
|
||||||
end
|
end
|
||||||
video_start ||= 0
|
video_start ||= 0
|
||||||
if query["time_continu"]?
|
if query["time_continue"]?
|
||||||
video_start = decode_time(query["t"])
|
video_start = decode_time(query["time_continue"])
|
||||||
end
|
end
|
||||||
video_start ||= 0
|
video_start ||= 0
|
||||||
if query["start"]?
|
if query["start"]?
|
||||||
@ -534,14 +564,25 @@ def process_video_params(query, preferences)
|
|||||||
raw ||= 0
|
raw ||= 0
|
||||||
raw = raw == 1
|
raw = raw == 1
|
||||||
|
|
||||||
quality = query["quality"]?
|
|
||||||
quality ||= "hd720"
|
|
||||||
|
|
||||||
controls = query["controls"]?.try &.to_i?
|
controls = query["controls"]?.try &.to_i?
|
||||||
controls ||= 1
|
controls ||= 1
|
||||||
controls = controls == 1
|
controls = controls == 1
|
||||||
|
|
||||||
return autoplay, video_loop, video_start, video_end, listen, raw, quality, controls
|
params = {
|
||||||
|
autoplay: autoplay,
|
||||||
|
controls: controls,
|
||||||
|
listen: listen,
|
||||||
|
preferred_captions: preferred_captions,
|
||||||
|
quality: quality,
|
||||||
|
raw: raw,
|
||||||
|
speed: speed,
|
||||||
|
video_end: video_end,
|
||||||
|
video_loop: video_loop,
|
||||||
|
video_start: video_start,
|
||||||
|
volume: volume,
|
||||||
|
}
|
||||||
|
|
||||||
|
return params
|
||||||
end
|
end
|
||||||
|
|
||||||
def generate_thumbnails(json, id)
|
def generate_thumbnails(json, id)
|
||||||
|
@ -16,21 +16,25 @@
|
|||||||
<p class="h-box">
|
<p class="h-box">
|
||||||
<% if user %>
|
<% if user %>
|
||||||
<% if subscriptions.includes? ucid %>
|
<% if subscriptions.includes? ucid %>
|
||||||
<a href="/subscription_ajax?action_remove_subscriptions=1&c=<%= ucid %>">
|
<a href="/subscription_ajax?action_remove_subscriptions=1&c=<%= ucid %>&referer=<%= env.get("current_page") %>">
|
||||||
<b>Unsubscribe from <%= author %></b>
|
<b>Unsubscribe from <%= author %></b>
|
||||||
</a>
|
</a>
|
||||||
<% else %>
|
<% else %>
|
||||||
<a href="/subscription_ajax?action_create_subscription_to_channel=1&c=<%= ucid %>">
|
<a href="/subscription_ajax?action_create_subscription_to_channel=1&c=<%= ucid %>&referer=<%= env.get("current_page") %>">
|
||||||
<b>Subscribe to <%= author %></b>
|
<b>Subscribe to <%= author %></b>
|
||||||
</a>
|
</a>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% else %>
|
<% else %>
|
||||||
<a href="/login">
|
<a href="/login?referer=<%= env.get("current_page") %>">
|
||||||
<b>Login to subscribe to <%= author %></b>
|
<b>Login to subscribe to <%= author %></b>
|
||||||
</a>
|
</a>
|
||||||
<% end %>
|
<% end %>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<p class="h-box">
|
||||||
|
<a href="https://www.youtube.com/channel/<%= ucid %>">View channel on YouTube</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
<% videos.each_slice(4) do |slice| %>
|
<% videos.each_slice(4) do |slice| %>
|
||||||
<div class="pure-g">
|
<div class="pure-g">
|
||||||
<% slice.each do |video| %>
|
<% slice.each do |video| %>
|
||||||
@ -41,14 +45,14 @@
|
|||||||
|
|
||||||
<div class="pure-g h-box">
|
<div class="pure-g h-box">
|
||||||
<div class="pure-u-1 pure-u-md-1-5">
|
<div class="pure-u-1 pure-u-md-1-5">
|
||||||
<% if page > 2 %>
|
<% if page >= 2 %>
|
||||||
<a href="/channel/<%= ucid %>?page=<%= page - 1 %>">Previous page</a>
|
<a href="/channel/<%= ucid %>?page=<%= page - 1 %>">Previous page</a>
|
||||||
<% else %>
|
|
||||||
<a href="/channel/<%= ucid %>">Previous page</a>
|
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<div class="pure-u-1 pure-u-md-3-5"></div>
|
<div class="pure-u-1 pure-u-md-3-5"></div>
|
||||||
<div style="text-align:right;" class="pure-u-1 pure-u-md-1-5">
|
<div style="text-align:right;" class="pure-u-1 pure-u-md-1-5">
|
||||||
|
<% if videos.size == 30 %>
|
||||||
<a href="/channel/<%= ucid %>?page=<%= page + 1 %>">Next page</a>
|
<a href="/channel/<%= ucid %>?page=<%= page + 1 %>">Next page</a>
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,19 +1,19 @@
|
|||||||
<video style="width:100%" playsinline poster="<%= thumbnail %>" title="<%= HTML.escape(video.title) %>"
|
<video style="width:100%" playsinline poster="<%= thumbnail %>" title="<%= HTML.escape(video.title) %>"
|
||||||
id="player" class="video-js"
|
id="player" class="video-js"
|
||||||
<% if autoplay %>autoplay<% end %>
|
<% if params[:autoplay] %>autoplay<% end %>
|
||||||
<% if video_loop %>loop<% end %>
|
<% if params[:video_loop] %>loop<% end %>
|
||||||
<% if controls %>controls<% end %>>
|
<% if params[:controls] %>controls<% end %>>
|
||||||
<% if hlsvp %>
|
<% if hlsvp %>
|
||||||
<source src="<%= hlsvp %>" type="application/x-mpegURL">
|
<source src="<%= hlsvp %>" type="application/x-mpegURL">
|
||||||
<% else %>
|
<% else %>
|
||||||
<% if listen %>
|
<% if params[:listen] %>
|
||||||
<% audio_streams.each_with_index do |fmt, i| %>
|
<% audio_streams.each_with_index do |fmt, i| %>
|
||||||
<source src="<%= fmt["url"] %>" type='<%= fmt["type"] %>' label="<%= fmt["bitrate"] %>k" selected="<%= i == 0 ? true : false %>">
|
<source src="<%= fmt["url"] %>" type='<%= fmt["type"] %>' label="<%= fmt["bitrate"] %>k" selected="<%= i == 0 ? true : false %>">
|
||||||
<% end %>
|
<% end %>
|
||||||
<% else %>
|
<% else %>
|
||||||
<% fmt_stream.each_with_index do |fmt, i| %>
|
<% fmt_stream.each_with_index do |fmt, i| %>
|
||||||
<% if preferences %>
|
<% if params[:quality] %>
|
||||||
<source src="<%= fmt["url"] %>" type='<%= fmt["type"] %>' label="<%= fmt["label"] %>" selected="<%= preferences.quality == fmt["label"].split(" - ")[0] %>">
|
<source src="<%= fmt["url"] %>" type='<%= fmt["type"] %>' label="<%= fmt["label"] %>" selected="<%= params[:quality] == fmt["label"].split(" - ")[0] %>">
|
||||||
<% else %>
|
<% else %>
|
||||||
<source src="<%= fmt["url"] %>" type='<%= fmt["type"] %>' label="<%= fmt["label"] %>" selected="<%= i == 0 ? true : false %>">
|
<source src="<%= fmt["url"] %>" type='<%= fmt["type"] %>' label="<%= fmt["label"] %>" selected="<%= i == 0 ? true : false %>">
|
||||||
<% end %>
|
<% end %>
|
||||||
@ -110,7 +110,7 @@ var player = videojs("player", options, function() {
|
|||||||
|
|
||||||
player.share(shareOptions);
|
player.share(shareOptions);
|
||||||
|
|
||||||
<% if video_start > 0 || video_end > 0 %>
|
<% if params[:video_start] > 0 || params[:video_end] > 0 %>
|
||||||
player.markers({
|
player.markers({
|
||||||
onMarkerReached: function(marker) {
|
onMarkerReached: function(marker) {
|
||||||
if (marker.text === "End") {
|
if (marker.text === "End") {
|
||||||
@ -122,19 +122,19 @@ player.markers({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
markers: [
|
markers: [
|
||||||
{ time: <%= video_start %>, text: "Start" },
|
{ time: <%= params[:video_start] %>, text: "Start" },
|
||||||
<% if video_end < 0 %>
|
<% if params[:video_end] < 0 %>
|
||||||
{ time: <%= video.info["length_seconds"].to_f - 0.5 %>, text: "End" }
|
{ time: <%= video.info["length_seconds"].to_f - 0.5 %>, text: "End" }
|
||||||
<% else %>
|
<% else %>
|
||||||
{ time: <%= video_end %>, text: "End" }
|
{ time: <%= params[:video_end] %>, text: "End" }
|
||||||
<% end %>
|
<% end %>
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
player.currentTime(<%= video_start %>);
|
player.currentTime(<%= params[:video_start] %>);
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<% if !listen %>
|
<% if !params[:listen] %>
|
||||||
var currentSources = player.currentSources();
|
var currentSources = player.currentSources();
|
||||||
for (var i = 0; i < currentSources.length; i++) {
|
for (var i = 0; i < currentSources.length; i++) {
|
||||||
if (player.canPlayType(currentSources[i]["type"].split(";")[0]) === "") {
|
if (player.canPlayType(currentSources[i]["type"].split(";")[0]) === "") {
|
||||||
@ -146,8 +146,6 @@ for (var i = 0; i < currentSources.length; i++) {
|
|||||||
player.src(currentSources);
|
player.src(currentSources);
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<% if preferences %>
|
player.volume(<%= params[:volume].to_f / 100 %>);
|
||||||
player.volume(<%= preferences.volume.to_f / 100 %>);
|
player.playbackRate(<%= params[:speed] %>);
|
||||||
player.playbackRate(<%= preferences.speed %>);
|
|
||||||
<% end %>
|
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,6 +1,11 @@
|
|||||||
<div class="pure-u-1 pure-u-md-1-4">
|
<div class="pure-u-1 pure-u-md-1-4">
|
||||||
<div class="h-box">
|
<div class="h-box">
|
||||||
<a style="width:100%;" href="/watch?v=<%= video.id %>">
|
<% if video.responds_to?(:playlists) && !video.playlists.empty? %>
|
||||||
|
<% params = "&list=#{video.playlists[0]}" %>
|
||||||
|
<% else %>
|
||||||
|
<% params = nil %>
|
||||||
|
<% end %>
|
||||||
|
<a style="width:100%;" href="/watch?v=<%= video.id %><%= params %>">
|
||||||
<% if env.get?("user") && env.get("user").as(User).preferences.thin_mode %>
|
<% if env.get?("user") && env.get("user").as(User).preferences.thin_mode %>
|
||||||
<% else %>
|
<% else %>
|
||||||
<img style="width:100%;" src="https://i.ytimg.com/vi/<%= video.id %>/mqdefault.jpg"/>
|
<img style="width:100%;" src="https://i.ytimg.com/vi/<%= video.id %>/mqdefault.jpg"/>
|
||||||
@ -15,4 +20,4 @@
|
|||||||
<h5>Shared <%= recode_date(video.published) %> ago</h5>
|
<h5>Shared <%= recode_date(video.published) %> ago</h5>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<div class="h-box">
|
<div class="h-box">
|
||||||
<form class="pure-form pure-form-aligned" enctype="multipart/form-data" action="/data_control" method="post">
|
<form class="pure-form pure-form-aligned" enctype="multipart/form-data" action="/data_control?referer=<%= referer %>" method="post">
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>Import</legend>
|
<legend>Import</legend>
|
||||||
|
|
||||||
@ -52,4 +52,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
@ -26,4 +26,4 @@
|
|||||||
<body>
|
<body>
|
||||||
<%= rendered "components/player" %>
|
<%= rendered "components/player" %>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -4,4 +4,4 @@
|
|||||||
|
|
||||||
<div class="h-box">
|
<div class="h-box">
|
||||||
<%= error_message %>
|
<%= error_message %>
|
||||||
</div>
|
</div>
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<hr>
|
<hr>
|
||||||
<% if account_type == "invidious" %>
|
<% if account_type == "invidious" %>
|
||||||
<form class="pure-form pure-form-stacked" action="/login?referer=<%= referer %>&type=invidious" method="post">
|
<form class="pure-form pure-form-stacked" action="/login?referer=<%= URI.escape(referer) %>&type=invidious" method="post">
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<label for="email">User ID:</label>
|
<label for="email">User ID:</label>
|
||||||
<input required class="pure-input-1" name="email" type="text" placeholder="User ID">
|
<input required class="pure-input-1" name="email" type="text" placeholder="User ID">
|
||||||
@ -34,7 +34,7 @@
|
|||||||
</fieldset>
|
</fieldset>
|
||||||
</form>
|
</form>
|
||||||
<% elsif account_type == "google" %>
|
<% elsif account_type == "google" %>
|
||||||
<form class="pure-form pure-form-stacked" action="/login?referer=<%= referer %>" method="post">
|
<form class="pure-form pure-form-stacked" action="/login?referer=<%= URI.escape(referer) %>" method="post">
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<label for="email">Email:</label>
|
<label for="email">Email:</label>
|
||||||
<input required class="pure-input-1" name="email" type="email" placeholder="Email">
|
<input required class="pure-input-1" name="email" type="email" placeholder="Email">
|
||||||
|
42
src/invidious/views/playlist.ecr
Normal file
42
src/invidious/views/playlist.ecr
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
<% content_for "header" do %>
|
||||||
|
<title><%= playlist.title %> - Invidious</title>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<div class="pure-g h-box">
|
||||||
|
<div class="pure-u-2-3">
|
||||||
|
<h3><%= playlist.title %></h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="pure-g h-box">
|
||||||
|
<div class="pure-u-1 pure-u-md-1-4">
|
||||||
|
<a href="/channel/<%= playlist.ucid %>">
|
||||||
|
<b><%= playlist.author %></b>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="h-box">
|
||||||
|
<p><%= playlist.description %></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<% videos.each_slice(4) do |slice| %>
|
||||||
|
<div class="pure-g">
|
||||||
|
<% slice.each do |video| %>
|
||||||
|
<%= rendered "components/video" %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<div class="pure-g h-box">
|
||||||
|
<div class="pure-u-1 pure-u-md-1-5">
|
||||||
|
<% if page >= 2 %>
|
||||||
|
<a href="/playlist?list=<%= playlist.id %>&page=<%= page - 1 %>">Next page</a>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<div class="pure-u-1 pure-u-md-3-5"></div>
|
||||||
|
<div style="text-align:right;" class="pure-u-1 pure-u-md-1-5">
|
||||||
|
<% if videos.size == 100 %>
|
||||||
|
<a href="/playlist?list=<%= playlist.id %>&page=<%= page + 1 %>">Next page</a>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -48,10 +48,19 @@ function update_value(element) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pure-control-group">
|
<div class="pure-control-group">
|
||||||
<label for="comments">Pull comments from: </label>
|
<label for="comments_0">Default comments: </label>
|
||||||
<select name="comments" id="comments">
|
<select name="comments_0" id="comments_0">
|
||||||
<% {"youtube", "reddit"}.each do |option| %>
|
<% {"", "youtube", "reddit"}.each do |option| %>
|
||||||
<option <% if user.preferences.comments == option %> selected <% end %>><%= option %></option>
|
<option <% if user.preferences.comments[0] == option %> selected <% end %>><%= option %></option>
|
||||||
|
<% end %>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pure-control-group">
|
||||||
|
<label for="comments_1">Fallback comments: </label>
|
||||||
|
<select name="comments_1" id="comments_1">
|
||||||
|
<% {"", "youtube", "reddit"}.each do |option| %>
|
||||||
|
<option <% if user.preferences.comments[1] == option %> selected <% end %>><%= option %></option>
|
||||||
<% end %>
|
<% end %>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@ -66,7 +75,7 @@ function update_value(element) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pure-control-group">
|
<div class="pure-control-group">
|
||||||
<label for="captions_fallback">Fallback languages: </label>
|
<label for="captions_fallback">Fallback captions: </label>
|
||||||
<select class="pure-u-1-5" name="captions_1" id="captions_1">
|
<select class="pure-u-1-5" name="captions_1" id="captions_1">
|
||||||
<% CAPTION_LANGUAGES.each do |option| %>
|
<% CAPTION_LANGUAGES.each do |option| %>
|
||||||
<option <% if user.preferences.captions[1] == option %> selected <% end %>><%= option %></option>
|
<option <% if user.preferences.captions[1] == option %> selected <% end %>><%= option %></option>
|
||||||
@ -80,7 +89,13 @@ function update_value(element) {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="pure-control-group">
|
||||||
|
<label for="related_videos">Show related videos? </label>
|
||||||
|
<input name="related_videos" id="related_videos" type="checkbox" <% if user.preferences.related_videos %>checked<% end %>>
|
||||||
|
</div>
|
||||||
|
|
||||||
<legend>Visual preferences</legend>
|
<legend>Visual preferences</legend>
|
||||||
|
|
||||||
<div class="pure-control-group">
|
<div class="pure-control-group">
|
||||||
<label for="dark_mode">Dark mode: </label>
|
<label for="dark_mode">Dark mode: </label>
|
||||||
<input name="dark_mode" id="dark_mode" type="checkbox" <% if user.preferences.dark_mode %>checked<% end %>>
|
<input name="dark_mode" id="dark_mode" type="checkbox" <% if user.preferences.dark_mode %>checked<% end %>>
|
||||||
@ -92,6 +107,7 @@ function update_value(element) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<legend>Subscription preferences</legend>
|
<legend>Subscription preferences</legend>
|
||||||
|
|
||||||
<div class="pure-control-group">
|
<div class="pure-control-group">
|
||||||
<label for="redirect_feed">Redirect homepage to feed: </label>
|
<label for="redirect_feed">Redirect homepage to feed: </label>
|
||||||
<input name="redirect_feed" id="redirect_feed" type="checkbox" <% if user.preferences.redirect_feed %>checked<% end %>>
|
<input name="redirect_feed" id="redirect_feed" type="checkbox" <% if user.preferences.redirect_feed %>checked<% end %>>
|
||||||
@ -127,12 +143,13 @@ function update_value(element) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<legend>Data preferences</legend>
|
<legend>Data preferences</legend>
|
||||||
|
|
||||||
<div class="pure-control-group">
|
<div class="pure-control-group">
|
||||||
<a href="/clear_watch_history">Clear watch history</a>
|
<a href="/clear_watch_history?referer=<%= referer %>">Clear watch history</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pure-control-group">
|
<div class="pure-control-group">
|
||||||
<a href="/data_control">Import/Export data</a>
|
<a href="/data_control?referer=<%= referer %>">Import/Export data</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pure-control-group">
|
<div class="pure-control-group">
|
||||||
@ -144,4 +161,4 @@ function update_value(element) {
|
|||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<% content_for "header" do %>
|
<% content_for "header" do %>
|
||||||
<title><%= query.not_nil!.size > 30 ? query.not_nil![0,30].rstrip(".") + "..." : query.not_nil! %> - Invidious</title>
|
<title><%= search_query.not_nil!.size > 30 ? query.not_nil![0,30].rstrip(".") + "..." : query.not_nil! %> - Invidious</title>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<% videos.each_slice(4) do |slice| %>
|
<% videos.each_slice(4) do |slice| %>
|
||||||
@ -12,14 +12,14 @@
|
|||||||
|
|
||||||
<div class="pure-g h-box">
|
<div class="pure-g h-box">
|
||||||
<div class="pure-u-1 pure-u-md-1-5">
|
<div class="pure-u-1 pure-u-md-1-5">
|
||||||
<% if page > 2 %>
|
<% if page >= 2 %>
|
||||||
<a href="/search?q=<%= query %>&page=<%= page - 1 %>">Previous page</a>
|
<a href="/search?q=<%= query %>&page=<%= page - 1 %>">Previous page</a>
|
||||||
<% else %>
|
|
||||||
<a href="/search?q=<%= query %>">Previous page</a>
|
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<div class="pure-u-1 pure-u-md-3-5"></div>
|
<div class="pure-u-1 pure-u-md-3-5"></div>
|
||||||
<div style="text-align:right;" class="pure-u-1 pure-u-md-1-5">
|
<div style="text-align:right;" class="pure-u-1 pure-u-md-1-5">
|
||||||
|
<% if count == 20 %>
|
||||||
<a href="/search?q=<%= query %>&page=<%= page + 1 %>">Next page</a>
|
<a href="/search?q=<%= query %>&page=<%= page + 1 %>">Next page</a>
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="pure-u-1-3" style="text-align:right;">
|
<div class="pure-u-1-3" style="text-align:right;">
|
||||||
<h3>
|
<h3>
|
||||||
<a href="/data_control">Import/Export</a>
|
<a href="/data_control?referer=<%= referer %>">Import/Export</a>
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -33,4 +33,4 @@
|
|||||||
<hr>
|
<hr>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
@ -16,6 +16,13 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<center><%= notifications.size %> unseen notifications</center>
|
<center><%= notifications.size %> unseen notifications</center>
|
||||||
|
|
||||||
|
<% if !notifications.empty? %>
|
||||||
|
<div class="h-box">
|
||||||
|
<hr>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
<% notifications.each_slice(4) do |slice| %>
|
<% notifications.each_slice(4) do |slice| %>
|
||||||
<div class="pure-g">
|
<div class="pure-g">
|
||||||
<% slice.each do |video| %>
|
<% slice.each do |video| %>
|
||||||
@ -38,14 +45,14 @@
|
|||||||
|
|
||||||
<div class="pure-g">
|
<div class="pure-g">
|
||||||
<div class="pure-u-1 pure-u-md-1-5">
|
<div class="pure-u-1 pure-u-md-1-5">
|
||||||
<% if page > 2 %>
|
<% if page >= 2 %>
|
||||||
<a href="/feed/subscriptions?max_results=<%= max_results %>&page=<%= page - 1 %>">Previous page</a>
|
<a href="/feed/subscriptions?max_results=<%= max_results %>&page=<%= page - 1 %>">Previous page</a>
|
||||||
<% else %>
|
|
||||||
<a href="/feed/subscriptions?max_results=<%= max_results %>">Previous page</a>
|
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<div class="pure-u-1 pure-u-md-3-5"></div>
|
<div class="pure-u-1 pure-u-md-3-5"></div>
|
||||||
<div style="text-align:right;" class="pure-u-1 pure-u-md-1-5">
|
<div style="text-align:right;" class="pure-u-1 pure-u-md-1-5">
|
||||||
<a href="/feed/subscriptions?max_results=<%= max_results %>&page=<%= page + 1 %>">Next page</a>
|
<% if (videos.size + notifications.size) == max_results %>
|
||||||
|
<a href="/feed/subscriptions?max_results=<%= max_results %>&page=<%= page + 1 %>">Next page</a>
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta name="referrer" content="no-referrer">
|
||||||
<%= yield_content "header" %>
|
<%= yield_content "header" %>
|
||||||
<link rel="stylesheet" href="https://unpkg.com/purecss@1.0.0/build/pure-min.css">
|
<link rel="stylesheet" href="https://unpkg.com/purecss@1.0.0/build/pure-min.css">
|
||||||
<link rel="stylesheet" href="https://unpkg.com/purecss@1.0.0/build/grids-responsive-min.css">
|
<link rel="stylesheet" href="https://unpkg.com/purecss@1.0.0/build/grids-responsive-min.css">
|
||||||
@ -34,7 +35,7 @@
|
|||||||
<div class="pure-u-1 pure-u-md-8-24 user-field">
|
<div class="pure-u-1 pure-u-md-8-24 user-field">
|
||||||
<% if env.get? "user" %>
|
<% if env.get? "user" %>
|
||||||
<div class="pure-u-1-4">
|
<div class="pure-u-1-4">
|
||||||
<a href="/toggle_theme" class="pure-menu-heading">
|
<a href="/toggle_theme?referer=<%= env.get?("current_page") %>" class="pure-menu-heading">
|
||||||
<% preferences = env.get("user").as(User).preferences %>
|
<% preferences = env.get("user").as(User).preferences %>
|
||||||
<% if preferences.dark_mode %>
|
<% if preferences.dark_mode %>
|
||||||
<i class="icon ion-ios-sunny"></i>
|
<i class="icon ion-ios-sunny"></i>
|
||||||
@ -54,15 +55,15 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="pure-u-1-4">
|
<div class="pure-u-1-4">
|
||||||
<a href="/preferences" class="pure-menu-heading">
|
<a href="/preferences?referer=<%= env.get?("current_page") %>" class="pure-menu-heading">
|
||||||
<i class="icon ion-ios-cog"></i>
|
<i class="icon ion-ios-cog"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="pure-u-1-4">
|
<div class="pure-u-1-4">
|
||||||
<a href="/signout" class="pure-menu-heading">Sign out</a>
|
<a href="/signout?referer=<%= env.get?("current_page") %>" class="pure-menu-heading">Sign out</a>
|
||||||
</div>
|
</div>
|
||||||
<% else %>
|
<% else %>
|
||||||
<a href="/login" class="pure-menu-heading">Login</a>
|
<a href="/login?referer=<%= env.get?("current_page") %>" class="pure-menu-heading">Login</a>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -72,6 +73,18 @@
|
|||||||
Roth</a>.
|
Roth</a>.
|
||||||
Source available <a
|
Source available <a
|
||||||
href="https://github.com/omarroth/invidious">here</a>.
|
href="https://github.com/omarroth/invidious">here</a>.
|
||||||
|
<p>Liberapay:
|
||||||
|
<a href="https://liberapay.com/omarroth">
|
||||||
|
https://liberapay.com/omarroth
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p>Patreon:
|
||||||
|
<a href="https://patreon.com/omarroth">
|
||||||
|
https://patreon.com/omarroth
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p>BTC: 356DpZyMXu6rYd55Yqzjs29n79kGKWcYrY</p>
|
||||||
|
<p>BCH: qq4ptclkzej5eza6a50et5ggc58hxsq5aylqut2npk</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="pure-u-1 pure-u-md-4-24"></div>
|
<div class="pure-u-1 pure-u-md-4-24"></div>
|
@ -30,6 +30,110 @@
|
|||||||
<%= rendered "components/player" %>
|
<%= rendered "components/player" %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="h-box">
|
||||||
|
<h1>
|
||||||
|
<%= HTML.escape(video.title) %>
|
||||||
|
<% if params[:listen] %>
|
||||||
|
<a href="/watch?<%= env.params.query %>">
|
||||||
|
<i class="icon ion-ios-videocam"></i>
|
||||||
|
</a>
|
||||||
|
<% else %>
|
||||||
|
<a href="/watch?<%= env.params.query %>&listen=1">
|
||||||
|
<i class="icon ion-ios-volume-high"></i>
|
||||||
|
</a>
|
||||||
|
<% end %>
|
||||||
|
</h1>
|
||||||
|
<% if !reason.empty? %>
|
||||||
|
<h3><%= reason %></h3>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pure-g">
|
||||||
|
<div class="pure-u-1 pure-u-md-1-5">
|
||||||
|
<div class="h-box">
|
||||||
|
<p><a href="https://www.youtube.com/watch?v=<%= video.id %>">Watch video on YouTube</a></p>
|
||||||
|
<p><i class="icon ion-ios-eye"></i> <%= number_with_separator(video.views) %></p>
|
||||||
|
<p><i class="icon ion-ios-thumbs-up"></i> <%= number_with_separator(video.likes) %></p>
|
||||||
|
<p><i class="icon ion-ios-thumbs-down"></i> <%= number_with_separator(video.dislikes) %></p>
|
||||||
|
<p id="Genre">Genre: <a href="<%= video.genre_url %>"><%= video.genre %></a></p>
|
||||||
|
<p id="FamilyFriendly">Family Friendly? <%= video.is_family_friendly %></p>
|
||||||
|
<p id="Wilson">Wilson Score: <%= video.wilson_score.round(4) %></p>
|
||||||
|
<p id="Rating">Rating: <%= rating.round(4) %> / 5</p>
|
||||||
|
<p id="Engagement">Engagement: <%= engagement.round(2) %>%</p>
|
||||||
|
<% if video.allowed_regions.size != REGIONS.size %>
|
||||||
|
<p id="AllowedRegions">
|
||||||
|
<% if video.allowed_regions.size < REGIONS.size / 2 %>
|
||||||
|
Whitelisted regions: <%= video.allowed_regions.join(", ") %>
|
||||||
|
<% else %>
|
||||||
|
Blacklisted regions: <%= (REGIONS.to_a - video.allowed_regions).join(", ") %>
|
||||||
|
<% end %>
|
||||||
|
</p>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pure-u-1 pure-u-md-3-5">
|
||||||
|
<div class="h-box">
|
||||||
|
<p>
|
||||||
|
<a href="/channel/<%= video.ucid %>">
|
||||||
|
<h3><%= video.author %></h3>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<% if user %>
|
||||||
|
<% if subscriptions.includes? video.ucid %>
|
||||||
|
<p>
|
||||||
|
<a href="/subscription_ajax?action_remove_subscriptions=1&c=<%= video.ucid %>&referer=<%= env.get("current_page") %>">
|
||||||
|
<b>Unsubscribe from <%= video.author %></b>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<% else %>
|
||||||
|
<p>
|
||||||
|
<a href="/subscription_ajax?action_create_subscription_to_channel=1&c=<%= video.ucid %>&referer=<%= env.get("current_page") %>">
|
||||||
|
<b>Subscribe to <%= video.author %></b>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<% end %>
|
||||||
|
<% else %>
|
||||||
|
<p>
|
||||||
|
<a href="/login?referer=<%= env.get("current_page") %>">
|
||||||
|
<b>Login to subscribe to <%= video.author %></b>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<% end %>
|
||||||
|
<p>
|
||||||
|
<b>Shared <%= video.published.to_s("%B %-d, %Y") %></b>
|
||||||
|
</p>
|
||||||
|
<div>
|
||||||
|
<%= video.description %>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
<div id="comments">
|
||||||
|
<h3><center class="loading"><i class="icon ion-ios-refresh"></i></center></h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="pure-u-1 pure-u-md-1-5">
|
||||||
|
<% if preferences && preferences.related_videos %>
|
||||||
|
<div class="h-box">
|
||||||
|
<% rvs.each do |rv| %>
|
||||||
|
<% if rv.has_key?("id") %>
|
||||||
|
<a href="/watch?v=<%= rv["id"] %>">
|
||||||
|
<% if preferences && preferences.thin_mode %>
|
||||||
|
<% else %>
|
||||||
|
<img style="width:100%;" src="<%= rv["iurlmq"] %>">
|
||||||
|
<% end %>
|
||||||
|
<p style="width:100%"><%= rv["title"] %></p>
|
||||||
|
<p>
|
||||||
|
<b style="width: 100%"><%= rv["author"] %></b>
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
function toggle(target) {
|
function toggle(target) {
|
||||||
body = target.parentNode.parentNode.children[1];
|
body = target.parentNode.parentNode.children[1];
|
||||||
@ -53,7 +157,7 @@ function toggle_comments(target) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function load_comments(target) {
|
function get_youtube_replies(target) {
|
||||||
var continuation = target.getAttribute("data-continuation");
|
var continuation = target.getAttribute("data-continuation");
|
||||||
|
|
||||||
var body = target.parentNode.parentNode;
|
var body = target.parentNode.parentNode;
|
||||||
@ -72,7 +176,7 @@ function load_comments(target) {
|
|||||||
xhr.onreadystatechange = function() {
|
xhr.onreadystatechange = function() {
|
||||||
if (xhr.readyState == 4) {
|
if (xhr.readyState == 4) {
|
||||||
if (xhr.status == 200) {
|
if (xhr.status == 200) {
|
||||||
body.innerHTML = xhr.response.content_html;
|
body.innerHTML = xhr.response.contentHtml;
|
||||||
} else {
|
} else {
|
||||||
body.innerHTML = fallback;
|
body.innerHTML = fallback;
|
||||||
}
|
}
|
||||||
@ -80,6 +184,8 @@ function load_comments(target) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
xhr.ontimeout = function() {
|
xhr.ontimeout = function() {
|
||||||
|
console.log("Pulling comments timed out.");
|
||||||
|
|
||||||
body.innerHTML = fallback;
|
body.innerHTML = fallback;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -106,19 +212,26 @@ function get_reddit_comments() {
|
|||||||
<a target="_blank" href="https://reddit.com{permalink}">View more comments on Reddit</a>
|
<a target="_blank" href="https://reddit.com{permalink}">View more comments on Reddit</a>
|
||||||
</b>
|
</b>
|
||||||
</div>
|
</div>
|
||||||
<div>{content_html}</div>
|
<div>{contentHtml}</div>
|
||||||
|
|
||||||
<hr>`.supplant({
|
<hr>`.supplant({
|
||||||
title: xhr.response.title,
|
title: xhr.response.title,
|
||||||
permalink: xhr.response.permalink,
|
permalink: xhr.response.permalink,
|
||||||
content_html: xhr.response.content_html
|
contentHtml: xhr.response.contentHtml
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
<% if preferences && preferences.comments[1] == "youtube" %>
|
||||||
get_youtube_comments();
|
get_youtube_comments();
|
||||||
|
<% else %>
|
||||||
|
comments = document.getElementById("comments");
|
||||||
|
comments.innerHTML = "";
|
||||||
|
<% end %>
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
xhr.ontimeout = function() {
|
xhr.ontimeout = function() {
|
||||||
|
console.log("Pulling comments timed out.");
|
||||||
|
|
||||||
get_reddit_comments();
|
get_reddit_comments();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -135,24 +248,35 @@ function get_youtube_comments() {
|
|||||||
if (xhr.readyState == 4)
|
if (xhr.readyState == 4)
|
||||||
if (xhr.status == 200) {
|
if (xhr.status == 200) {
|
||||||
comments = document.getElementById("comments");
|
comments = document.getElementById("comments");
|
||||||
comments.innerHTML = `
|
if (xhr.response.commentCount > 0) {
|
||||||
<div>
|
comments.innerHTML = `
|
||||||
<h3>
|
<div>
|
||||||
<a href="javascript:void(0)" onclick="toggle_comments(this)">[ - ]</a>
|
<h3>
|
||||||
<a target="_blank" href="https://www.youtube.com/watch?v=<%= video.id %>">View more comments on YouTube</a>
|
<a href="javascript:void(0)" onclick="toggle_comments(this)">[ - ]</a>
|
||||||
</h3>
|
View {commentCount} comments
|
||||||
</div>
|
</h3>
|
||||||
<div>{content_html}</div>
|
</div>
|
||||||
<hr>`.supplant({
|
<div>{contentHtml}</div>
|
||||||
content_html: xhr.response.content_html
|
<hr>`.supplant({
|
||||||
});
|
contentHtml: xhr.response.contentHtml,
|
||||||
|
commentCount: commaSeparateNumber(xhr.response.commentCount)
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
comments.innerHTML = "";
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
|
<% if preferences && preferences.comments[1] == "youtube" %>
|
||||||
|
get_youtube_comments();
|
||||||
|
<% else %>
|
||||||
comments = document.getElementById("comments");
|
comments = document.getElementById("comments");
|
||||||
comments.innerHTML = "";
|
comments.innerHTML = "";
|
||||||
|
<% end %>
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
xhr.ontimeout = function() {
|
xhr.ontimeout = function() {
|
||||||
|
console.log("Pulling comments timed out.");
|
||||||
|
|
||||||
comments = document.getElementById("comments");
|
comments = document.getElementById("comments");
|
||||||
comments.innerHTML =
|
comments.innerHTML =
|
||||||
'<h3><center class="loading"><i class="icon ion-ios-refresh"></i></center></h3>';
|
'<h3><center class="loading"><i class="icon ion-ios-refresh"></i></center></h3>';
|
||||||
@ -160,6 +284,13 @@ function get_youtube_comments() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function commaSeparateNumber(val){
|
||||||
|
while (/(\d+)(\d{3})/.test(val.toString())){
|
||||||
|
val = val.toString().replace(/(\d+)(\d{3})/, '$1'+','+'$2');
|
||||||
|
}
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
|
||||||
String.prototype.supplant = function(o) {
|
String.prototype.supplant = function(o) {
|
||||||
return this.replace(/{([^{}]*)}/g, function(a, b) {
|
return this.replace(/{([^{}]*)}/g, function(a, b) {
|
||||||
var r = o[b];
|
var r = o[b];
|
||||||
@ -167,112 +298,23 @@ String.prototype.supplant = function(o) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
<% if preferences && preferences.comments == "reddit" %>
|
<% if preferences %>
|
||||||
get_reddit_comments();
|
<% if preferences.comments[0] == "youtube" %>
|
||||||
|
get_youtube_comments();
|
||||||
|
<% elsif preferences.comments[0] == "reddit" %>
|
||||||
|
get_reddit_comments();
|
||||||
|
<% else %>
|
||||||
|
<% if preferences.comments[1] == "youtube" %>
|
||||||
|
get_youtube_comments();
|
||||||
|
<% elsif preferences.comments[1] == "reddit" %>
|
||||||
|
get_reddit_comments();
|
||||||
|
<% else %>
|
||||||
|
comments = document.getElementById("comments");
|
||||||
|
comments.innerHTML = "";
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
<% else %>
|
<% else %>
|
||||||
get_youtube_comments();
|
get_youtube_comments();
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="h-box">
|
|
||||||
<h1>
|
|
||||||
<%= HTML.escape(video.title) %>
|
|
||||||
<% if listen %>
|
|
||||||
<a href="/watch?<%= env.params.query %>">
|
|
||||||
<i class="icon ion-ios-videocam"></i>
|
|
||||||
</a>
|
|
||||||
<% else %>
|
|
||||||
<a href="/watch?<%= env.params.query %>&listen=1">
|
|
||||||
<i class="icon ion-ios-volume-high"></i>
|
|
||||||
</a>
|
|
||||||
<% end %>
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="pure-g">
|
|
||||||
<div class="pure-u-1 pure-u-md-1-5">
|
|
||||||
<div class="h-box">
|
|
||||||
<p><a href="https://www.youtube.com/watch?v=<%= video.id %>">Watch video on YouTube</a></p>
|
|
||||||
<p><i class="icon ion-ios-eye"></i> <%= number_with_separator(video.views) %></p>
|
|
||||||
<p><i class="icon ion-ios-thumbs-up"></i> <%= number_with_separator(video.likes) %></p>
|
|
||||||
<p><i class="icon ion-ios-thumbs-down"></i> <%= number_with_separator(video.dislikes) %></p>
|
|
||||||
<p id="Genre">Genre: <%= video.genre %></p>
|
|
||||||
<p id="FamilyFriendly">Family Friendly? <%= video.is_family_friendly %></p>
|
|
||||||
<p id="Wilson">Wilson Score: <%= video.wilson_score.round(4) %></p>
|
|
||||||
<p id="Rating">Rating: <%= rating.round(4) %> / 5</p>
|
|
||||||
<p id="Engagement">Engagement: <%= engagement.round(2) %>%</p>
|
|
||||||
<% if video.allowed_regions.size != REGIONS.size %>
|
|
||||||
<p id="AllowedRegions">
|
|
||||||
<% if video.allowed_regions.size < REGIONS.size / 2 %>
|
|
||||||
Whitelisted regions: <%= video.allowed_regions.join(", ") %>
|
|
||||||
<% else %>
|
|
||||||
Blacklisted regions: <%= (REGIONS.to_a - video.allowed_regions).join(", ") %>
|
|
||||||
<% end %>
|
|
||||||
</p>
|
|
||||||
<% end %>
|
|
||||||
<% if engage_types %>
|
|
||||||
<p id="Engage">Engage Types: <%= engage_types %></p>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="pure-u-1 pure-u-md-3-5">
|
|
||||||
<div class="h-box">
|
|
||||||
<p>
|
|
||||||
<a href="/channel/<%= video.ucid %>">
|
|
||||||
<h3><%= video.author %></h3>
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
<% if user %>
|
|
||||||
<% if subscriptions.includes? video.ucid %>
|
|
||||||
<p>
|
|
||||||
<a href="/subscription_ajax?action_remove_subscriptions=1&c=<%= video.ucid %>">
|
|
||||||
<b>Unsubscribe from <%= video.author %></b>
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
<% else %>
|
|
||||||
<p>
|
|
||||||
<a href="/subscription_ajax?action_create_subscription_to_channel=1&c=<%= video.ucid %>">
|
|
||||||
<b>Subscribe to <%= video.author %></b>
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
<% end %>
|
|
||||||
<% else %>
|
|
||||||
<p>
|
|
||||||
<a href="/login">
|
|
||||||
<b>Login to subscribe to <%= video.author %></b>
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
<% end %>
|
|
||||||
<p>
|
|
||||||
<b>Shared <%= video.published.to_s("%B %-d, %Y") %></b>
|
|
||||||
</p>
|
|
||||||
<div>
|
|
||||||
<%= video.description %>
|
|
||||||
</div>
|
|
||||||
<hr>
|
|
||||||
<div id="comments">
|
|
||||||
<h3><center class="loading"><i class="icon ion-ios-refresh"></i></center></h3>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="pure-u-1 pure-u-md-1-5">
|
|
||||||
<div class="h-box">
|
|
||||||
<% rvs.each do |rv| %>
|
|
||||||
<% if rv.has_key?("id") %>
|
|
||||||
<a href="/watch?v=<%= rv["id"] %>">
|
|
||||||
<% if preferences && preferences.thin_mode %>
|
|
||||||
<% else %>
|
|
||||||
<img style="width:100%;" src="<%= rv["iurlmq"] %>">
|
|
||||||
<% end %>
|
|
||||||
<p style="width:100%"><%= rv["title"] %></p>
|
|
||||||
<p>
|
|
||||||
<b style="width: 100%"><%= rv["author"] %></b>
|
|
||||||
</p>
|
|
||||||
</a>
|
|
||||||
<% end %>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
Reference in New Issue
Block a user