summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/ci.yml3
-rw-r--r--README.md2
-rw-r--r--assets/js/watch.js2
-rw-r--r--locales/ar.json10
-rw-r--r--locales/en-US.json2
-rw-r--r--locales/eo.json8
-rw-r--r--locales/es.json10
-rw-r--r--locales/ja.json16
-rw-r--r--locales/ko.json10
-rw-r--r--locales/lt.json8
-rw-r--r--locales/pt.json8
-rw-r--r--locales/tr.json8
-rw-r--r--locales/zh-CN.json8
-rw-r--r--locales/zh-TW.json8
-rw-r--r--src/invidious/helpers/extractors.cr2
-rw-r--r--src/invidious/helpers/serialized_yt_data.cr9
-rw-r--r--src/invidious/playlists.cr49
-rw-r--r--src/invidious/routes/api/v1/misc.cr30
-rw-r--r--src/invidious/views/components/item.ecr2
-rw-r--r--src/invidious/views/template.ecr4
20 files changed, 150 insertions, 49 deletions
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 3bb4c491..b99ecf18 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -40,6 +40,7 @@ jobs:
crystal:
- 1.0.0
- 1.1.1
+ - 1.2.0
include:
- crystal: nightly
stable: false
@@ -48,7 +49,7 @@ jobs:
- uses: actions/checkout@v2
- name: Install Crystal
- uses: oprypin/install-crystal@v1.2.4
+ uses: crystal-lang/install-crystal@v1.5.3
with:
crystal: ${{ matrix.crystal }}
diff --git a/README.md b/README.md
index 3f7fa8a7..85a2704a 100644
--- a/README.md
+++ b/README.md
@@ -58,7 +58,7 @@
- No JavaScript required
- Light/Dark themes
- Customizable homepage
-- Subscriptions independant from Google
+- Subscriptions independent from Google
- Notifications for all subscribed channels
- Audio-only mode (with background play on mobile)
- Support for Reddit comments
diff --git a/assets/js/watch.js b/assets/js/watch.js
index 3909edd4..1579abf4 100644
--- a/assets/js/watch.js
+++ b/assets/js/watch.js
@@ -149,6 +149,8 @@ function get_playlist(plid, retries) {
if (xhr.readyState == 4) {
if (xhr.status == 200) {
playlist.innerHTML = xhr.response.playlistHtml;
+ var nextVideo = document.getElementById(xhr.response.nextVideo);
+ nextVideo.parentNode.parentNode.scrollTop = nextVideo.offsetTop;
if (xhr.response.nextVideo) {
player.on('ended', function () {
diff --git a/locales/ar.json b/locales/ar.json
index 4f7f1e2c..457c648b 100644
--- a/locales/ar.json
+++ b/locales/ar.json
@@ -382,7 +382,7 @@
"News": "الأخبار",
"Movies": "الأفلام",
"Download": "نزّل",
- "Download as: ": "نزله كـ:. ",
+ "Download as: ": "نزله ك:. ",
"%A %B %-d, %Y": "%A %-d %B %Y",
"(edited)": "(تم تعديلة)",
"YouTube comment permalink": "رابط التعليق على اليوتيوب",
@@ -425,5 +425,11 @@
"next_steps_error_message_refresh": "تحديث",
"next_steps_error_message_go_to_youtube": "انتقل إلى يوتيوب",
"short": "قصير (< 4 دقائق)",
- "long": "طويل (> 20 دقيقة)"
+ "long": "طويل (> 20 دقيقة)",
+ "footer_source_code": "شفرة المصدر",
+ "footer_original_source_code": "شفرة المصدر الأصلية",
+ "footer_modfied_source_code": "شفرة المصدر المعدلة",
+ "adminprefs_modified_source_code_url_label": "URL إلى مستودع التعليمات البرمجية المصدرية المعدلة",
+ "footer_documentation": "التوثيق",
+ "footer_donate": "تبرّع: "
}
diff --git a/locales/en-US.json b/locales/en-US.json
index 1fa1983d..8d213c7a 100644
--- a/locales/en-US.json
+++ b/locales/en-US.json
@@ -426,7 +426,7 @@
"next_steps_error_message": "After which you should try to: ",
"next_steps_error_message_refresh": "Refresh",
"next_steps_error_message_go_to_youtube": "Go to YouTube",
- "footer_donate": "Donate: ",
+ "footer_donate_page": "Donate",
"footer_documentation": "Documentation",
"footer_source_code": "Source code",
"footer_original_source_code": "Original source code",
diff --git a/locales/eo.json b/locales/eo.json
index a00da6aa..5c1d3b52 100644
--- a/locales/eo.json
+++ b/locales/eo.json
@@ -425,5 +425,11 @@
"next_steps_error_message_refresh": "Reŝargi",
"next_steps_error_message_go_to_youtube": "Iri al JuTubo",
"long": "Longa (> 20 minutos)",
- "short": "Mallonga (< 4 minutos)"
+ "short": "Mallonga (< 4 minutos)",
+ "footer_donate": "Doni: ",
+ "footer_documentation": "Dokumentaro",
+ "footer_source_code": "Fontkodo",
+ "adminprefs_modified_source_code_url_label": "URL al modifita deponejo de fontkodo",
+ "footer_modfied_source_code": "Modifita Fontkodo",
+ "footer_original_source_code": "Originala fontkodo"
}
diff --git a/locales/es.json b/locales/es.json
index 3cbe9be4..7255a8bc 100644
--- a/locales/es.json
+++ b/locales/es.json
@@ -424,6 +424,12 @@
"next_steps_error_message": "Después de lo cual deberías intentar: ",
"next_steps_error_message_refresh": "Recargar",
"next_steps_error_message_go_to_youtube": "Ir a YouTube",
- "short": "Corto (< minutos)",
- "long": "Largo (> minutos)"
+ "short": "Corto (< 4 minutos)",
+ "long": "Largo (> 20 minutos)",
+ "footer_documentation": "Documentación",
+ "footer_original_source_code": "Código fuente original",
+ "adminprefs_modified_source_code_url_label": "URL al repositorio de código fuente modificado",
+ "footer_source_code": "Código fuente",
+ "footer_donate": "Donar: ",
+ "footer_modfied_source_code": "Código fuente modificado"
}
diff --git a/locales/ja.json b/locales/ja.json
index c4f78f96..4c2e692d 100644
--- a/locales/ja.json
+++ b/locales/ja.json
@@ -78,7 +78,7 @@
"Show related videos: ": "関連動画を表示: ",
"Show annotations by default: ": "デフォルトでアノテーションを表示: ",
"Automatically extend video description: ": "動画の説明文を自動的に拡張: ",
- "Interactive 360 degree videos: ": "インタラクティブ360°動画: ",
+ "Interactive 360 degree videos: ": "対話的な360°動画: ",
"Visual preferences": "外観設定",
"Player style: ": "プレイヤースタイル: ",
"Dark mode: ": "ダークモード: ",
@@ -137,7 +137,7 @@
},
"Import/export": "インポート/エクスポート",
"unsubscribe": "登録解除",
- "revoke": "revoke",
+ "revoke": "取り消す",
"Subscriptions": "登録チャンネル",
"`x` unseen notifications": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` 個の未読通知",
@@ -145,7 +145,7 @@
},
"search": "検索",
"Log out": "ログアウト",
- "Released under the AGPLv3 on Github.": "Github 上で AGPLv3 の下で公開されています",
+ "Released under the AGPLv3 on Github.": "Github 上で AGPLv3 の元で公開されています",
"Source available here.": "ソースはここで閲覧可能です。",
"View JavaScript license information.": "JavaScript ライセンス情報",
"View privacy policy.": "プライバシーポリシー",
@@ -423,5 +423,13 @@
"Current version: ": "現在のバージョン: ",
"next_steps_error_message": "下記のものを試して下さい: ",
"next_steps_error_message_refresh": "再読込",
- "next_steps_error_message_go_to_youtube": "YouTubeへ"
+ "next_steps_error_message_go_to_youtube": "YouTubeへ",
+ "short": "4 分未満",
+ "footer_donate": "寄金: ",
+ "footer_documentation": "文書",
+ "footer_source_code": "ソースコード",
+ "footer_original_source_code": "ソースコード(元)",
+ "footer_modfied_source_code": "ソースコード(編集)",
+ "adminprefs_modified_source_code_url_label": "編集したソースコードのレポジトリーURL",
+ "long": "20 分以上"
}
diff --git a/locales/ko.json b/locales/ko.json
index 16d4797e..27fdc683 100644
--- a/locales/ko.json
+++ b/locales/ko.json
@@ -423,5 +423,13 @@
"today": "오늘",
"hour": "지난 1시간",
"sort": "정렬기준",
- "features": "기능별"
+ "features": "기능별",
+ "short": "4분 미만",
+ "long": "20분 초과",
+ "footer_donate": "후원: ",
+ "footer_documentation": "문서",
+ "footer_source_code": "소스 코드",
+ "footer_original_source_code": "원본 소스 코드",
+ "footer_modfied_source_code": "수정된 소스 코드",
+ "adminprefs_modified_source_code_url_label": "수정된 소스 코드 저장소의 URL"
}
diff --git a/locales/lt.json b/locales/lt.json
index 4b2fa5aa..c1842e54 100644
--- a/locales/lt.json
+++ b/locales/lt.json
@@ -425,5 +425,11 @@
"next_steps_error_message_refresh": "Atnaujinti",
"next_steps_error_message_go_to_youtube": "Eiti į YouTube",
"short": "Trumpas (< 4 minučių)",
- "long": "Ilgas (> 20 minučių)"
+ "long": "Ilgas (> 20 minučių)",
+ "footer_documentation": "Dokumentacija",
+ "footer_source_code": "Pirminis kodas",
+ "footer_donate": "Paaukoti: ",
+ "footer_original_source_code": "Pradinis pirminis kodas",
+ "adminprefs_modified_source_code_url_label": "URL į pakeisto pirminio kodo repozitoriją",
+ "footer_modfied_source_code": "Pakeistas pirminis kodas"
}
diff --git a/locales/pt.json b/locales/pt.json
index 66de7d10..918ab4c0 100644
--- a/locales/pt.json
+++ b/locales/pt.json
@@ -425,5 +425,11 @@
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` subscritores"
},
"short": "Curto (< 4 minutos)",
- "long": "Longo (> 20 minutos)"
+ "long": "Longo (> 20 minutos)",
+ "footer_source_code": "Código-fonte",
+ "footer_original_source_code": "Código-fonte original",
+ "adminprefs_modified_source_code_url_label": "URL do repositório do código-fonte alterado",
+ "footer_donate": "Fazer um donativo: ",
+ "footer_documentation": "Documentação",
+ "footer_modfied_source_code": "Código-fonte alterado"
}
diff --git a/locales/tr.json b/locales/tr.json
index 8432e8a9..26c6abdd 100644
--- a/locales/tr.json
+++ b/locales/tr.json
@@ -425,5 +425,11 @@
"next_steps_error_message_refresh": "Yenile",
"next_steps_error_message_go_to_youtube": "YouTube'a git",
"short": "Kısa (4 dakikadan az)",
- "long": "Uzun (20 dakikadan fazla)"
+ "long": "Uzun (20 dakikadan fazla)",
+ "footer_donate": "Bağış yap: ",
+ "footer_documentation": "Belgelendirme",
+ "footer_source_code": "Kaynak kodları",
+ "footer_original_source_code": "Orijinal kaynak kodları",
+ "footer_modfied_source_code": "Değiştirilmiş kaynak kodları",
+ "adminprefs_modified_source_code_url_label": "Değiştirilmiş kaynak kodları deposunun URL'si"
}
diff --git a/locales/zh-CN.json b/locales/zh-CN.json
index 1033e77e..918b7c68 100644
--- a/locales/zh-CN.json
+++ b/locales/zh-CN.json
@@ -425,5 +425,11 @@
"next_steps_error_message_refresh": "刷新",
"next_steps_error_message_go_to_youtube": "转到 YouTube",
"short": "短(少于4分钟)",
- "long": "长(多于 20 分钟)"
+ "long": "长(多于 20 分钟)",
+ "footer_donate": "捐赠: ",
+ "footer_documentation": "文档",
+ "footer_source_code": "源代码",
+ "footer_modfied_source_code": "修改的源代码",
+ "adminprefs_modified_source_code_url_label": "更改的源代码仓库网址",
+ "footer_original_source_code": "原始源代码"
}
diff --git a/locales/zh-TW.json b/locales/zh-TW.json
index 403221c4..51de7090 100644
--- a/locales/zh-TW.json
+++ b/locales/zh-TW.json
@@ -425,5 +425,11 @@
"next_steps_error_message_refresh": "重新整理",
"next_steps_error_message_go_to_youtube": "到 YouTube",
"short": "短(小於4分鐘)",
- "long": "長(多於20分鐘)"
+ "long": "長(多於20分鐘)",
+ "footer_donate": "抖內: ",
+ "footer_documentation": "文件",
+ "footer_source_code": "原始碼",
+ "footer_original_source_code": "原本的原始碼",
+ "footer_modfied_source_code": "修改後的原始碼",
+ "adminprefs_modified_source_code_url_label": "修改後的原始碼倉庫 URL"
}
diff --git a/src/invidious/helpers/extractors.cr b/src/invidious/helpers/extractors.cr
index c8a6cd4a..1ebbe889 100644
--- a/src/invidious/helpers/extractors.cr
+++ b/src/invidious/helpers/extractors.cr
@@ -43,7 +43,7 @@ private module Parsers
private def self.parse(item_contents, author_fallback)
video_id = item_contents["videoId"].as_s
- title = extract_text(item_contents["title"]) || ""
+ title = extract_text(item_contents["title"]?) || ""
# Extract author information
if author_info = item_contents.dig?("ownerText", "runs", 0)
diff --git a/src/invidious/helpers/serialized_yt_data.cr b/src/invidious/helpers/serialized_yt_data.cr
index 61356555..a9798f0c 100644
--- a/src/invidious/helpers/serialized_yt_data.cr
+++ b/src/invidious/helpers/serialized_yt_data.cr
@@ -237,8 +237,15 @@ class Category
def to_json(locale, json : JSON::Builder)
json.object do
+ json.field "type", "category"
json.field "title", self.title
- json.field "contents", self.contents
+ json.field "contents" do
+ json.array do
+ self.contents.each do |item|
+ item.to_json(locale, json)
+ end
+ end
+ end
end
end
diff --git a/src/invidious/playlists.cr b/src/invidious/playlists.cr
index f56cc2ea..5034844e 100644
--- a/src/invidious/playlists.cr
+++ b/src/invidious/playlists.cr
@@ -107,7 +107,7 @@ struct Playlist
property updated : Time
property thumbnail : String?
- def to_json(offset, locale, json : JSON::Builder, continuation : String? = nil)
+ def to_json(offset, locale, json : JSON::Builder, video_id : String? = nil)
json.object do
json.field "type", "playlist"
json.field "title", self.title
@@ -142,7 +142,7 @@ struct Playlist
json.field "videos" do
json.array do
- videos = get_playlist_videos(PG_DB, self, offset: offset, locale: locale, continuation: continuation)
+ videos = get_playlist_videos(PG_DB, self, offset: offset, locale: locale, video_id: video_id)
videos.each_with_index do |video, index|
video.to_json(locale, json)
end
@@ -151,12 +151,12 @@ struct Playlist
end
end
- def to_json(offset, locale, json : JSON::Builder? = nil, continuation : String? = nil)
+ def to_json(offset, locale, json : JSON::Builder? = nil, video_id : String? = nil)
if json
- to_json(offset, locale, json, continuation: continuation)
+ to_json(offset, locale, json, video_id: video_id)
else
JSON.build do |json|
- to_json(offset, locale, json, continuation: continuation)
+ to_json(offset, locale, json, video_id: video_id)
end
end
end
@@ -196,7 +196,7 @@ struct InvidiousPlaylist
end
end
- def to_json(offset, locale, json : JSON::Builder, continuation : String? = nil)
+ def to_json(offset, locale, json : JSON::Builder, video_id : String? = nil)
json.object do
json.field "type", "invidiousPlaylist"
json.field "title", self.title
@@ -218,11 +218,11 @@ struct InvidiousPlaylist
json.field "videos" do
json.array do
if !offset || offset == 0
- index = PG_DB.query_one?("SELECT index FROM playlist_videos WHERE plid = $1 AND id = $2 LIMIT 1", self.id, continuation, as: Int64)
+ index = PG_DB.query_one?("SELECT index FROM playlist_videos WHERE plid = $1 AND id = $2 LIMIT 1", self.id, video_id, as: Int64)
offset = self.index.index(index) || 0
end
- videos = get_playlist_videos(PG_DB, self, offset: offset, locale: locale, continuation: continuation)
+ videos = get_playlist_videos(PG_DB, self, offset: offset, locale: locale, video_id: video_id)
videos.each_with_index do |video, index|
video.to_json(locale, json, offset + index)
end
@@ -231,12 +231,12 @@ struct InvidiousPlaylist
end
end
- def to_json(offset, locale, json : JSON::Builder? = nil, continuation : String? = nil)
+ def to_json(offset, locale, json : JSON::Builder? = nil, video_id : String? = nil)
if json
- to_json(offset, locale, json, continuation: continuation)
+ to_json(offset, locale, json, video_id: video_id)
else
JSON.build do |json|
- to_json(offset, locale, json, continuation: continuation)
+ to_json(offset, locale, json, video_id: video_id)
end
end
end
@@ -426,7 +426,7 @@ def fetch_playlist(plid, locale)
})
end
-def get_playlist_videos(db, playlist, offset, locale = nil, continuation = nil)
+def get_playlist_videos(db, playlist, offset, locale = nil, video_id = nil)
# Show empy playlist if requested page is out of range
# (e.g, when a new playlist has been created, offset will be negative)
if offset >= playlist.video_count || offset < 0
@@ -437,17 +437,26 @@ def get_playlist_videos(db, playlist, offset, locale = nil, continuation = nil)
db.query_all("SELECT * FROM playlist_videos WHERE plid = $1 ORDER BY array_position($2, index) LIMIT 100 OFFSET $3",
playlist.id, playlist.index, offset, as: PlaylistVideo)
else
- if offset >= 100
- # Normalize offset to match youtube's behavior (100 videos chunck per request)
- offset = (offset / 100).to_i64 * 100_i64
+ if video_id
+ initial_data = YoutubeAPI.next({
+ "videoId" => video_id,
+ "playlistId" => playlist.id,
+ })
+ offset = initial_data.dig?("contents", "twoColumnWatchNextResults", "playlist", "playlist", "currentIndex").try &.as_i || offset
+ end
+
+ videos = [] of PlaylistVideo
+ until videos.size >= 200 || videos.size == playlist.video_count || offset >= playlist.video_count
+ # 100 videos per request
ctoken = produce_playlist_continuation(playlist.id, offset)
initial_data = YoutubeAPI.browse(ctoken)
- else
- initial_data = YoutubeAPI.browse("VL" + playlist.id, params: "")
+ videos += extract_playlist_videos(initial_data)
+
+ offset += 100
end
- return extract_playlist_videos(initial_data)
+ return videos
end
end
@@ -523,8 +532,8 @@ def template_playlist(playlist)
playlist["videos"].as_a.each do |video|
html += <<-END_HTML
- <li class="pure-menu-item">
- <a href="/watch?v=#{video["videoId"]}&list=#{playlist["playlistId"]}">
+ <li class="pure-menu-item" id="#{video["videoId"]}">
+ <a href="/watch?v=#{video["videoId"]}&list=#{playlist["playlistId"]}&index=#{video["index"]}">
<div class="thumbnail">
<img class="thumbnail" src="/vi/#{video["videoId"]}/mqdefault.jpg">
<p class="length">#{recode_length_seconds(video["lengthSeconds"].as_i)}</p>
diff --git a/src/invidious/routes/api/v1/misc.cr b/src/invidious/routes/api/v1/misc.cr
index cf95bd9b..80b59fd5 100644
--- a/src/invidious/routes/api/v1/misc.cr
+++ b/src/invidious/routes/api/v1/misc.cr
@@ -24,7 +24,7 @@ module Invidious::Routes::API::V1::Misc
offset ||= env.params.query["page"]?.try &.to_i?.try { |page| (page - 1) * 100 }
offset ||= 0
- continuation = env.params.query["continuation"]?
+ video_id = env.params.query["continuation"]?
format = env.params.query["format"]?
format ||= "json"
@@ -46,12 +46,32 @@ module Invidious::Routes::API::V1::Misc
return error_json(404, "Playlist does not exist.")
end
- response = playlist.to_json(offset, locale, continuation: continuation)
+ # includes into the playlist a maximum of 20 videos, before the offset
+ if offset > 0
+ lookback = offset < 50 ? offset : 50
+ response = playlist.to_json(offset - lookback, locale)
+ json_response = JSON.parse(response)
+ else
+ # Unless the continuation is really the offset 0, it becomes expensive.
+ # It happens when the offset is not set.
+ # First we find the actual offset, and then we lookback
+ # it shouldn't happen often though
+
+ lookback = 0
+ response = playlist.to_json(offset, locale, video_id: video_id)
+ json_response = JSON.parse(response)
+
+ if json_response["videos"].as_a[0]["index"] != offset
+ offset = json_response["videos"].as_a[0]["index"].as_i
+ lookback = offset < 50 ? offset : 50
+ response = playlist.to_json(offset - lookback, locale)
+ json_response = JSON.parse(response)
+ end
+ end
if format == "html"
- response = JSON.parse(response)
- playlist_html = template_playlist(response)
- index, next_video = response["videos"].as_a.skip(1).select { |video| !video["author"].as_s.empty? }[0]?.try { |v| {v["index"], v["videoId"]} } || {nil, nil}
+ playlist_html = template_playlist(json_response)
+ index, next_video = json_response["videos"].as_a.skip(1 + lookback).select { |video| !video["author"].as_s.empty? }[0]?.try { |v| {v["index"], v["videoId"]} } || {nil, nil}
response = {
"playlistHtml" => playlist_html,
diff --git a/src/invidious/views/components/item.ecr b/src/invidious/views/components/item.ecr
index b15ae255..d084bfd4 100644
--- a/src/invidious/views/components/item.ecr
+++ b/src/invidious/views/components/item.ecr
@@ -48,7 +48,7 @@
<p dir="auto"><b><%= HTML.escape(item.author) %></b></p>
</a>
<% when PlaylistVideo %>
- <a style="width:100%" href="/watch?v=<%= item.id %>&list=<%= item.plid %>">
+ <a style="width:100%" href="/watch?v=<%= item.id %>&list=<%= item.plid %>&index=<%= item.index %>">
<% if !env.get("preferences").as(Preferences).thin_mode %>
<div class="thumbnail">
<img class="thumbnail" src="/vi/<%= item.id %>/mqdefault.jpg"/>
diff --git a/src/invidious/views/template.ecr b/src/invidious/views/template.ecr
index b7020598..3fb2fe18 100644
--- a/src/invidious/views/template.ecr
+++ b/src/invidious/views/template.ecr
@@ -149,9 +149,7 @@
<div class="pure-u-1 pure-u-md-1-3">
<span>
<i class="icon ion-ios-wallet"></i>
- <%= translate(locale, "footer_donate") %>
- <a href="bitcoin:bc1qfhe7rq3lqzuayzjxzyt9waz9ytrs09kla3tsgr">BTC</a>&nbsp;/
- <a href="monero:41nMCtek197boJtiUvGnTFYMatrLEpnpkQDmUECqx5Es2uX3sTKKWVhSL76suXsG3LXqkEJBrCZBgPTwJrDp1FrZJfycGPR">XMR</a>
+ <a href="https://invidious.io/donate/"><%= translate(locale, "footer_donate_page") %></a>
</span>
<span><%= translate(locale, "Current version: ") %> <%= CURRENT_VERSION %>-<%= CURRENT_COMMIT %> @ <%= CURRENT_BRANCH %></span>
</div>