forked from midou/invidious
Compare commits
30 Commits
Author | SHA1 | Date | |
---|---|---|---|
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 |
@ -2,9 +2,9 @@
|
||||
|
||||
-- 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,
|
||||
notifications 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",
|
||||
token text COLLATE pg_catalog."default",
|
||||
watched text[] COLLATE pg_catalog."default",
|
||||
CONSTRAINT users_email_key UNIQUE (email),
|
||||
CONSTRAINT users_id_key UNIQUE (id)
|
||||
CONSTRAINT users_email_key UNIQUE (email)
|
||||
)
|
||||
WITH (
|
||||
OIDS = FALSE
|
||||
|
@ -11,14 +11,11 @@ targets:
|
||||
dependencies:
|
||||
kemal:
|
||||
github: kemalcr/kemal
|
||||
branch: rework-param-parser
|
||||
pg:
|
||||
github: will/crystal-pg
|
||||
branch: master
|
||||
detect_language:
|
||||
github: detectlanguage/detectlanguage-crystal
|
||||
branch: master
|
||||
|
||||
crystal: 0.25.1
|
||||
crystal: 0.26.0
|
||||
|
||||
license: AGPLv3
|
||||
|
198
src/invidious.cr
198
src/invidious.cr
@ -114,10 +114,11 @@ before_all do |env|
|
||||
|
||||
# Invidious users only have SID
|
||||
if !env.request.cookies.has_key? "SSID"
|
||||
user = PG_DB.query_one?("SELECT * FROM users WHERE id = $1", sid, as: User)
|
||||
user = PG_DB.query_one?("SELECT * FROM users WHERE $1 = ANY(id)", sid, as: User)
|
||||
|
||||
if user
|
||||
env.set "user", user
|
||||
env.set "sid", sid
|
||||
end
|
||||
else
|
||||
begin
|
||||
@ -125,10 +126,24 @@ before_all do |env|
|
||||
user = get_user(sid, client, headers, PG_DB, false)
|
||||
|
||||
env.set "user", user
|
||||
env.set "sid", sid
|
||||
rescue ex
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
current_page = env.request.path
|
||||
if env.request.query
|
||||
query = HTTP::Params.parse(env.request.query.not_nil!)
|
||||
|
||||
if query["referer"]?
|
||||
query["referer"] = get_referer(env, "/")
|
||||
end
|
||||
|
||||
current_page += "?#{query}"
|
||||
end
|
||||
|
||||
env.set "current_page", URI.escape(current_page)
|
||||
end
|
||||
|
||||
get "/" do |env|
|
||||
@ -264,10 +279,11 @@ get "/watch" do |env|
|
||||
rating = video.info["avg_rating"].to_f64
|
||||
engagement = ((video.dislikes.to_f + video.likes.to_f)/video.views * 100)
|
||||
|
||||
if video.info["enabled_engage_types"]?
|
||||
engage_types = video.info["enabled_engage_types"].split(",")
|
||||
engage_types = engage_types.join(", ")
|
||||
playability_status = video.player_response["playabilityStatus"]?
|
||||
if playability_status && playability_status["status"] == "LIVE_STREAM_OFFLINE"
|
||||
reason = playability_status["reason"]?.try &.as_s
|
||||
end
|
||||
reason ||= ""
|
||||
|
||||
templated "watch"
|
||||
end
|
||||
@ -358,6 +374,31 @@ get "/embed/:id" do |env|
|
||||
rendered "embed"
|
||||
end
|
||||
|
||||
# Playlists
|
||||
get "/playlist" do |env|
|
||||
plid = env.params.query["list"]?
|
||||
if !plid
|
||||
next env.redirect "/"
|
||||
end
|
||||
|
||||
page = env.params.query["page"]?.try &.to_i?
|
||||
page ||= 1
|
||||
|
||||
if plid
|
||||
begin
|
||||
videos = extract_playlist(plid, page)
|
||||
rescue ex
|
||||
error_message = ex.message
|
||||
next templated "error"
|
||||
end
|
||||
playlist = fetch_playlist(plid)
|
||||
else
|
||||
next env.redirect "/"
|
||||
end
|
||||
|
||||
templated "playlist"
|
||||
end
|
||||
|
||||
# Search
|
||||
|
||||
get "/results" do |env|
|
||||
@ -414,8 +455,7 @@ end
|
||||
|
||||
# See https://github.com/rg3/youtube-dl/blob/master/youtube_dl/extractor/youtube.py#L79
|
||||
post "/login" do |env|
|
||||
referer = env.params.query["referer"]?
|
||||
referer ||= get_referer(env, "/feed/subscriptions")
|
||||
referer = get_referer(env, "/feed/subscriptions")
|
||||
|
||||
email = env.params.body["email"]?
|
||||
password = env.params.body["password"]?
|
||||
@ -509,7 +549,7 @@ post "/login" do |env|
|
||||
end
|
||||
|
||||
if !tfa_code
|
||||
next env.redirect "/login?tfa=true&type=google"
|
||||
next env.redirect "/login?tfa=true&type=google&referer=#{URI.escape(referer)}"
|
||||
end
|
||||
|
||||
tl = challenge_results[1][2]
|
||||
@ -621,8 +661,8 @@ post "/login" do |env|
|
||||
end
|
||||
|
||||
if Crypto::Bcrypt::Password.new(user.password.not_nil!) == password
|
||||
sid = Base64.encode(Random::Secure.random_bytes(50))
|
||||
PG_DB.exec("UPDATE users SET id = $1 WHERE email = $2", sid, email)
|
||||
sid = Base64.urlsafe_encode(Random::Secure.random_bytes(32))
|
||||
PG_DB.exec("UPDATE users SET id = id || $1 WHERE email = $2", [sid], email)
|
||||
|
||||
if Kemal.config.ssl || CONFIG.https_only
|
||||
secure = true
|
||||
@ -643,7 +683,7 @@ post "/login" do |env|
|
||||
next templated "error"
|
||||
end
|
||||
|
||||
sid = Base64.encode(Random::Secure.random_bytes(50))
|
||||
sid = Base64.urlsafe_encode(Random::Secure.random_bytes(32))
|
||||
user = create_user(sid, email, password)
|
||||
user_array = user.to_a
|
||||
|
||||
@ -673,8 +713,14 @@ get "/signout" do |env|
|
||||
cookie.expires = Time.new(1990, 1, 1)
|
||||
end
|
||||
|
||||
if env.get? "user"
|
||||
user = env.get("user").as(User)
|
||||
sid = env.get("sid").as(String)
|
||||
PG_DB.exec("UPDATE users SET id = array_remove(id, $1) WHERE email = $2", sid, user.email)
|
||||
end
|
||||
|
||||
env.request.cookies.add_response_headers(env.response.headers)
|
||||
env.redirect referer
|
||||
env.redirect URI.unescape(referer)
|
||||
end
|
||||
|
||||
get "/preferences" do |env|
|
||||
@ -865,7 +911,7 @@ get "/subscription_manager" do |env|
|
||||
headers["Cookie"] = env.request.headers["Cookie"]
|
||||
|
||||
client = make_client(YT_URL)
|
||||
user = get_user(user.id, client, headers, PG_DB)
|
||||
user = get_user(user.id[0], client, headers, PG_DB)
|
||||
end
|
||||
|
||||
action_takeout = env.params.query["action_takeout"]?.try &.to_i?
|
||||
@ -1173,7 +1219,7 @@ get "/feed/subscriptions" do |env|
|
||||
|
||||
if !user.password
|
||||
client = make_client(YT_URL)
|
||||
user = get_user(user.id, client, headers, PG_DB)
|
||||
user = get_user(user.id[0], client, headers, PG_DB)
|
||||
end
|
||||
|
||||
max_results = preferences.max_results
|
||||
@ -1340,7 +1386,7 @@ get "/feed/channel/:ucid" do |env|
|
||||
end
|
||||
|
||||
nodeset = document.xpath_nodes(%q(//li[contains(@class, "feed-item-container")]))
|
||||
extract_videos(nodeset).each do |video|
|
||||
extract_videos(nodeset, ucid).each do |video|
|
||||
xml.element("entry") do
|
||||
xml.element("id") { xml.text "yt:video:#{video.id}" }
|
||||
xml.element("yt:videoId") { xml.text video.id }
|
||||
@ -1519,31 +1565,13 @@ get "/channel/:ucid" do |env|
|
||||
rss = XML.parse_html(rss.body)
|
||||
author = rss.xpath_node("//feed/author/name").not_nil!.content
|
||||
|
||||
url = produce_playlist_url(ucid, (page - 1) * 100)
|
||||
response = client.get(url)
|
||||
response = JSON.parse(response.body)
|
||||
|
||||
if !response["content_html"]?
|
||||
error_message = "This channel does not exist."
|
||||
begin
|
||||
videos = extract_playlist(ucid, page)
|
||||
rescue ex
|
||||
error_message = ex.message
|
||||
next templated "error"
|
||||
end
|
||||
|
||||
document = XML.parse_html(response["content_html"].as_s)
|
||||
anchor = document.xpath_node(%q(//div[@class="pl-video-owner"]/a))
|
||||
if !anchor
|
||||
videos = [] of ChannelVideo
|
||||
next templated "channel"
|
||||
end
|
||||
|
||||
videos = [] of ChannelVideo
|
||||
document.xpath_nodes(%q(//a[contains(@class,"pl-video-title-link")])).each do |node|
|
||||
href = URI.parse(node["href"])
|
||||
id = HTTP::Params.parse(href.query.not_nil!)["v"]
|
||||
title = node.content
|
||||
|
||||
videos << ChannelVideo.new(id, title, Time.now, Time.now, "", "")
|
||||
end
|
||||
|
||||
templated "channel"
|
||||
end
|
||||
|
||||
@ -1682,7 +1710,7 @@ get "/api/v1/comments/:id" do |env|
|
||||
if format == "json"
|
||||
next {"comments" => [] of String}.to_json
|
||||
else
|
||||
next {"content_html" => ""}.to_json
|
||||
next {"contentHtml" => ""}.to_json
|
||||
end
|
||||
end
|
||||
ctoken = ctoken["ctoken"]
|
||||
@ -1720,7 +1748,7 @@ get "/api/v1/comments/:id" do |env|
|
||||
if format == "json"
|
||||
next {"comments" => [] of String}.to_json
|
||||
else
|
||||
next {"content_html" => ""}.to_json
|
||||
next {"contentHtml" => ""}.to_json
|
||||
end
|
||||
end
|
||||
|
||||
@ -1785,9 +1813,13 @@ get "/api/v1/comments/:id" do |env|
|
||||
json.field "commentId", node_comment["commentId"]
|
||||
|
||||
if node_replies && !response["commentRepliesContinuation"]?
|
||||
reply_count = node_replies["moreText"]["simpleText"].as_s.match(/View all (?<count>\d+) replies/)
|
||||
.try &.["count"].to_i?
|
||||
reply_count ||= 1
|
||||
reply_count = node_replies["moreText"]["simpleText"].as_s.delete("View all reply replies,")
|
||||
if reply_count.empty?
|
||||
reply_count = 1
|
||||
else
|
||||
reply_count = reply_count.try &.to_i?
|
||||
reply_count ||= 1
|
||||
end
|
||||
|
||||
continuation = node_replies["continuations"].as_a[0]["nextContinuationData"]["continuation"].as_s
|
||||
|
||||
@ -1816,7 +1848,17 @@ get "/api/v1/comments/:id" do |env|
|
||||
comments = JSON.parse(comments)
|
||||
content_html = template_youtube_comments(comments)
|
||||
|
||||
next {"content_html" => content_html}.to_json
|
||||
response = JSON.build do |json|
|
||||
json.object do
|
||||
json.field "contentHtml", content_html
|
||||
|
||||
if comments["commentCount"]?
|
||||
json.field "commentCount", comments["commentCount"]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
next response
|
||||
end
|
||||
elsif source == "reddit"
|
||||
client = make_client(REDDIT_URL)
|
||||
@ -1837,9 +1879,10 @@ get "/api/v1/comments/:id" do |env|
|
||||
end
|
||||
|
||||
env.response.content_type = "application/json"
|
||||
next {"title" => reddit_thread.title,
|
||||
"permalink" => reddit_thread.permalink,
|
||||
"content_html" => content_html}.to_json
|
||||
next {"title" => reddit_thread.title,
|
||||
"permalink" => reddit_thread.permalink,
|
||||
"contentHtml" => content_html,
|
||||
}.to_json
|
||||
end
|
||||
end
|
||||
|
||||
@ -1868,7 +1911,7 @@ get "/api/v1/videos/:id" do |env|
|
||||
generate_thumbnails(json, video.id)
|
||||
end
|
||||
|
||||
description = html_to_description(video.description)
|
||||
description, video.description = html_to_description(video.description)
|
||||
|
||||
json.field "description", description
|
||||
json.field "descriptionHtml", video.description
|
||||
@ -2343,6 +2386,65 @@ get "/api/v1/search" do |env|
|
||||
response
|
||||
end
|
||||
|
||||
get "/api/v1/playlists/:plid" do |env|
|
||||
plid = env.params.url["plid"]
|
||||
|
||||
page = env.params.query["page"]?.try &.to_i?
|
||||
page ||= 1
|
||||
|
||||
begin
|
||||
videos = extract_playlist(plid, page)
|
||||
rescue ex
|
||||
env.response.content_type = "application/json"
|
||||
response = {"error" => "Playlist is empty"}.to_json
|
||||
halt env, status_code: 404, response: response
|
||||
end
|
||||
|
||||
playlist = fetch_playlist(plid)
|
||||
|
||||
response = JSON.build do |json|
|
||||
json.object do
|
||||
json.field "title", playlist.title
|
||||
json.field "id", playlist.id
|
||||
|
||||
json.field "author", playlist.author
|
||||
json.field "authorId", playlist.ucid
|
||||
json.field "authorUrl", "/channel/#{playlist.ucid}"
|
||||
|
||||
json.field "description", playlist.description
|
||||
json.field "videoCount", playlist.video_count
|
||||
|
||||
json.field "viewCount", playlist.views
|
||||
json.field "updated", playlist.updated.epoch
|
||||
|
||||
json.field "videos" do
|
||||
json.array do
|
||||
videos.each do |video|
|
||||
json.object do
|
||||
json.field "title", video.title
|
||||
json.field "id", video.id
|
||||
|
||||
json.field "author", video.author
|
||||
json.field "authorId", video.ucid
|
||||
json.field "authorUrl", "/channel/#{video.ucid}"
|
||||
|
||||
json.field "videoThumbnails" do
|
||||
generate_thumbnails(json, video.id)
|
||||
end
|
||||
|
||||
json.field "index", video.index
|
||||
json.field "lengthSeconds", video.length_seconds
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
env.response.content_type = "application/json"
|
||||
response
|
||||
end
|
||||
|
||||
get "/api/manifest/dash/id/videoplayback" do |env|
|
||||
env.response.headers["Access-Control-Allow-Origin"] = "*"
|
||||
env.redirect "/videoplayback?#{env.params.query}"
|
||||
@ -2611,10 +2713,6 @@ if Kemal.config.ssl
|
||||
server.bind_tcp "0.0.0.0", 80
|
||||
server.listen
|
||||
end
|
||||
|
||||
before_all do |env|
|
||||
env.response.headers.add("Strict-Transport-Security", "max-age=31536000; includeSubDomains; preload")
|
||||
end
|
||||
end
|
||||
|
||||
static_headers do |response, filepath, filestat|
|
||||
|
@ -103,7 +103,7 @@ def template_youtube_comments(comments)
|
||||
html += <<-END_HTML
|
||||
<div class="pure-g">
|
||||
<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 class="pure-u-22-24">
|
||||
<p>
|
||||
@ -262,7 +262,7 @@ def fill_links(html, scheme, host)
|
||||
end
|
||||
|
||||
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
|
||||
html = html.to_xml(options: XML::SaveOptions::NO_DECL)
|
||||
end
|
||||
|
@ -116,39 +116,6 @@ def login_req(login_form, f_req)
|
||||
return HTTP::Params.encode(data)
|
||||
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}"
|
||||
|
||||
@ -268,7 +235,7 @@ def generate_captcha(key)
|
||||
|
||||
answer = "#{hour}:#{minute.to_s.rjust(2, '0')}"
|
||||
token = OpenSSL::HMAC.digest(:sha256, key, answer)
|
||||
token = Base64.encode(token)
|
||||
token = Base64.urlsafe_encode(token)
|
||||
|
||||
return {challenge: challenge, token: token}
|
||||
end
|
||||
@ -301,6 +268,15 @@ def extract_videos(nodeset, ucid = nil)
|
||||
next
|
||||
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
|
||||
id = anchor["href"].lchop("/watch?v=")
|
||||
|
||||
@ -317,38 +293,31 @@ def extract_videos(nodeset, ucid = nil)
|
||||
author_id = anchor["href"].split("/")[-1]
|
||||
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))
|
||||
if metadata.size == 0
|
||||
if metadata.empty?
|
||||
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
|
||||
|
||||
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, description_html = html_to_description(description_html)
|
||||
|
||||
|
@ -9,7 +9,7 @@ macro add_mapping(mapping)
|
||||
DB.mapping({{mapping}})
|
||||
end
|
||||
|
||||
macro templated(filename, template = "layout")
|
||||
macro templated(filename, template = "template")
|
||||
render "src/invidious/views/#{{{filename}}}.ecr", "src/invidious/views/#{{{template}}}.ecr"
|
||||
end
|
||||
|
||||
|
@ -31,11 +31,11 @@ class HTTPProxy
|
||||
|
||||
if resp[:code]? == 200
|
||||
{% if !flag?(:without_openssl) %}
|
||||
if tls
|
||||
tls_socket = OpenSSL::SSL::Socket::Client.new(socket, context: tls, sync_close: true, hostname: host)
|
||||
socket = tls_socket
|
||||
end
|
||||
{% end %}
|
||||
if tls
|
||||
tls_socket = OpenSSL::SSL::Socket::Client.new(socket, context: tls, sync_close: true, hostname: host)
|
||||
socket = tls_socket
|
||||
end
|
||||
{% end %}
|
||||
|
||||
return socket
|
||||
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-Language"] = "Accept-Language: en-US,en;q=0.9"
|
||||
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"
|
||||
body = {
|
||||
"xpp" => "2",
|
||||
"xpp" => "5",
|
||||
"xf1" => "0",
|
||||
"xf2" => "2",
|
||||
"xf4" => "1",
|
||||
"xf2" => "0",
|
||||
"xf4" => "0",
|
||||
"xf5" => "1",
|
||||
}
|
||||
response = client.post("/free-proxy-list/#{country_code}/", headers, form: 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}
|
||||
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|
|
||||
if !node["onmouseover"]?
|
||||
next
|
||||
end
|
||||
|
||||
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
|
||||
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
|
||||
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)
|
||||
# String matches 'YYYY'
|
||||
if string.match(/\d{4}/)
|
||||
if string.match(/^\d{4}/)
|
||||
return Time.new(string.to_i, 1, 1)
|
||||
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"...
|
||||
date = string.split(" ")[-3, 3]
|
||||
delta = date[0].to_i
|
||||
@ -150,10 +163,27 @@ def make_host_url(ssl, host)
|
||||
end
|
||||
|
||||
def get_referer(env, fallback = "/")
|
||||
referer = env.request.headers["referer"]?
|
||||
referer = env.params.query["referer"]?
|
||||
referer ||= env.request.headers["referer"]?
|
||||
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
|
||||
referer = fallback
|
||||
|
160
src/invidious/playlists.cr
Normal file
160
src/invidious/playlists.cr
Normal file
@ -0,0 +1,160 @@
|
||||
class Playlist
|
||||
add_mapping({
|
||||
title: String,
|
||||
id: String,
|
||||
author: String,
|
||||
ucid: String,
|
||||
description: 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.to_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 = document.xpath_node(%q(//span[@class="pl-header-description-text"]/div/div[1]))
|
||||
description ||= document.xpath_node(%q(//span[@class="pl-header-description-text"]))
|
||||
|
||||
if description
|
||||
description = description.to_xml.strip(" \n")
|
||||
description = description.split("<button ")[0]
|
||||
description = fill_links(description, "https", "www.youtube.com")
|
||||
description = add_alt_links(description)
|
||||
else
|
||||
description = ""
|
||||
end
|
||||
|
||||
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,
|
||||
video_count,
|
||||
views,
|
||||
updated
|
||||
)
|
||||
|
||||
return playlist
|
||||
end
|
@ -10,7 +10,7 @@ class User
|
||||
end
|
||||
|
||||
add_mapping({
|
||||
id: String,
|
||||
id: Array(String),
|
||||
updated: Time,
|
||||
notifications: Array(String),
|
||||
subscriptions: Array(String),
|
||||
@ -78,8 +78,8 @@ class Preferences
|
||||
end
|
||||
|
||||
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)
|
||||
user = db.query_one("SELECT * FROM users WHERE id = $1", sid, as: User)
|
||||
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 $1 = ANY(id)", sid, as: User)
|
||||
|
||||
if refresh && Time.now - user.updated > 1.minute
|
||||
user = fetch_user(sid, client, headers, db)
|
||||
@ -89,7 +89,7 @@ def get_user(sid, client, headers, db, refresh = true)
|
||||
args = arg_array(user_array)
|
||||
|
||||
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
|
||||
else
|
||||
user = fetch_user(sid, client, headers, db)
|
||||
@ -99,7 +99,7 @@ def get_user(sid, client, headers, db, refresh = true)
|
||||
args = arg_array(user.to_a)
|
||||
|
||||
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
|
||||
|
||||
return user
|
||||
@ -132,7 +132,7 @@ def fetch_user(sid, client, headers, db)
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
@ -140,7 +140,7 @@ def create_user(sid, email, password)
|
||||
password = Crypto::Bcrypt::Password.create(password, cost: 10)
|
||||
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
|
||||
end
|
||||
|
@ -228,6 +228,8 @@ VIDEO_FORMATS = {
|
||||
}
|
||||
|
||||
class Video
|
||||
property player_json : JSON::Any?
|
||||
|
||||
module HTTPParamConverter
|
||||
def self.from_rs(rs)
|
||||
HTTP::Params.parse(rs.read(String))
|
||||
@ -287,9 +289,15 @@ class Video
|
||||
return audio_streams
|
||||
end
|
||||
|
||||
def captions
|
||||
player_response = JSON.parse(self.info["player_response"])
|
||||
def player_response
|
||||
if !@player_json
|
||||
@player_json = JSON.parse(@info["player_response"])
|
||||
end
|
||||
|
||||
return @player_json.not_nil!
|
||||
end
|
||||
|
||||
def captions
|
||||
captions = [] of Caption
|
||||
if player_response["captions"]?
|
||||
caption_list = player_response["captions"]["playerCaptionsTracklistRenderer"]["captionTracks"]?.try &.as_a
|
||||
|
@ -16,21 +16,25 @@
|
||||
<p class="h-box">
|
||||
<% if user %>
|
||||
<% 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>
|
||||
</a>
|
||||
<% 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>
|
||||
</a>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<a href="/login">
|
||||
<a href="/login?referer=<%= env.get("current_page") %>">
|
||||
<b>Login to subscribe to <%= author %></b>
|
||||
</a>
|
||||
<% end %>
|
||||
</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| %>
|
||||
<div class="pure-g">
|
||||
<% slice.each do |video| %>
|
||||
@ -41,14 +45,14 @@
|
||||
|
||||
<div class="pure-g h-box">
|
||||
<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>
|
||||
<% else %>
|
||||
<a href="/channel/<%= ucid %>">Previous 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="/channel/<%= ucid %>?page=<%= page + 1 %>">Next page</a>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,6 +1,11 @@
|
||||
<div class="pure-u-1 pure-u-md-1-4">
|
||||
<div class="h-box">
|
||||
<a style="width:100%;" href="/watch?v=<%= video.id %>">
|
||||
<% if video.responds_to?(:playlists) %>
|
||||
<% 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 %>
|
||||
<% else %>
|
||||
<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>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -52,4 +52,4 @@
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -26,4 +26,4 @@
|
||||
<body>
|
||||
<%= rendered "components/player" %>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
@ -4,4 +4,4 @@
|
||||
|
||||
<div class="h-box">
|
||||
<%= error_message %>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -16,7 +16,7 @@
|
||||
</div>
|
||||
<hr>
|
||||
<% 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>
|
||||
<label for="email">User ID:</label>
|
||||
<input required class="pure-input-1" name="email" type="text" placeholder="User ID">
|
||||
@ -34,7 +34,7 @@
|
||||
</fieldset>
|
||||
</form>
|
||||
<% 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>
|
||||
<label for="email">Email:</label>
|
||||
<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>
|
@ -144,4 +144,4 @@ function update_value(element) {
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -12,14 +12,12 @@
|
||||
|
||||
<div class="pure-g h-box">
|
||||
<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>
|
||||
<% else %>
|
||||
<a href="/search?q=<%= query %>">Previous 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">
|
||||
<a href="/search?q=<%= query %>&page=<%= page + 1 %>">Next page</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -33,4 +33,4 @@
|
||||
<hr>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
@ -38,14 +38,14 @@
|
||||
|
||||
<div class="pure-g">
|
||||
<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>
|
||||
<% else %>
|
||||
<a href="/feed/subscriptions?max_results=<%= max_results %>">Previous 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">
|
||||
<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>
|
||||
|
@ -34,7 +34,7 @@
|
||||
<div class="pure-u-1 pure-u-md-8-24 user-field">
|
||||
<% if env.get? "user" %>
|
||||
<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 %>
|
||||
<% if preferences.dark_mode %>
|
||||
<i class="icon ion-ios-sunny"></i>
|
||||
@ -54,15 +54,15 @@
|
||||
</a>
|
||||
</div>
|
||||
<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>
|
||||
</a>
|
||||
</div>
|
||||
<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>
|
||||
<% 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 %>
|
||||
</div>
|
||||
</div>
|
||||
@ -72,6 +72,13 @@
|
||||
Roth</a>.
|
||||
Source available <a
|
||||
href="https://github.com/omarroth/invidious">here</a>.
|
||||
<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 class="pure-u-1 pure-u-md-4-24"></div>
|
@ -72,7 +72,7 @@ function load_comments(target) {
|
||||
xhr.onreadystatechange = function() {
|
||||
if (xhr.readyState == 4) {
|
||||
if (xhr.status == 200) {
|
||||
body.innerHTML = xhr.response.content_html;
|
||||
body.innerHTML = xhr.response.contentHtml;
|
||||
} else {
|
||||
body.innerHTML = fallback;
|
||||
}
|
||||
@ -106,12 +106,12 @@ function get_reddit_comments() {
|
||||
<a target="_blank" href="https://reddit.com{permalink}">View more comments on Reddit</a>
|
||||
</b>
|
||||
</div>
|
||||
<div>{content_html}</div>
|
||||
<div>{contentHtml}</div>
|
||||
|
||||
<hr>`.supplant({
|
||||
title: xhr.response.title,
|
||||
permalink: xhr.response.permalink,
|
||||
content_html: xhr.response.content_html
|
||||
contentHtml: xhr.response.contentHtml
|
||||
});
|
||||
} else {
|
||||
get_youtube_comments();
|
||||
@ -139,12 +139,13 @@ function get_youtube_comments() {
|
||||
<div>
|
||||
<h3>
|
||||
<a href="javascript:void(0)" onclick="toggle_comments(this)">[ - ]</a>
|
||||
<a target="_blank" href="https://www.youtube.com/watch?v=<%= video.id %>">View more comments on YouTube</a>
|
||||
View {commentCount} comments
|
||||
</h3>
|
||||
</div>
|
||||
<div>{content_html}</div>
|
||||
<div>{contentHtml}</div>
|
||||
<hr>`.supplant({
|
||||
content_html: xhr.response.content_html
|
||||
contentHtml: xhr.response.contentHtml,
|
||||
commentCount: commaSeparateNumber(xhr.response.commentCount)
|
||||
});
|
||||
} else {
|
||||
comments = document.getElementById("comments");
|
||||
@ -160,6 +161,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) {
|
||||
return this.replace(/{([^{}]*)}/g, function(a, b) {
|
||||
var r = o[b];
|
||||
@ -188,6 +196,9 @@ get_youtube_comments();
|
||||
</a>
|
||||
<% end %>
|
||||
</h1>
|
||||
<% if !reason.empty? %>
|
||||
<h3><%= reason %></h3>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="pure-g">
|
||||
@ -211,9 +222,6 @@ get_youtube_comments();
|
||||
<% end %>
|
||||
</p>
|
||||
<% end %>
|
||||
<% if engage_types %>
|
||||
<p id="Engage">Engage Types: <%= engage_types %></p>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -227,20 +235,20 @@ get_youtube_comments();
|
||||
<% if user %>
|
||||
<% if subscriptions.includes? video.ucid %>
|
||||
<p>
|
||||
<a href="/subscription_ajax?action_remove_subscriptions=1&c=<%= video.ucid %>">
|
||||
<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 %>">
|
||||
<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">
|
||||
<a href="/login?referer=<%= env.get("current_page") %>">
|
||||
<b>Login to subscribe to <%= video.author %></b>
|
||||
</a>
|
||||
</p>
|
||||
|
Reference in New Issue
Block a user