diff options
| -rw-r--r-- | assets/css/default.css | 24 | ||||
| -rw-r--r-- | config/config.example.yml | 10 | ||||
| -rw-r--r-- | locales/cs.json | 2 | ||||
| -rw-r--r-- | locales/es.json | 21 | ||||
| -rw-r--r-- | locales/ko.json | 15 | ||||
| -rw-r--r-- | src/invidious/channels/channels.cr | 36 | ||||
| -rw-r--r-- | src/invidious/comments.cr | 73 | ||||
| -rw-r--r-- | src/invidious/database/users.cr | 2 | ||||
| -rw-r--r-- | src/invidious/helpers/utils.cr | 53 | ||||
| -rw-r--r-- | src/invidious/routes/watch.cr | 6 | ||||
| -rw-r--r-- | src/invidious/trending.cr | 19 | ||||
| -rw-r--r-- | src/invidious/user/imports.cr | 2 | ||||
| -rw-r--r-- | src/invidious/videos/description.cr | 53 | ||||
| -rw-r--r-- | src/invidious/videos/parser.cr | 2 | ||||
| -rw-r--r-- | src/invidious/yt_backend/extractors_utils.cr | 13 |
15 files changed, 179 insertions, 152 deletions
diff --git a/assets/css/default.css b/assets/css/default.css index 42f6958f..23649f8f 100644 --- a/assets/css/default.css +++ b/assets/css/default.css @@ -322,6 +322,30 @@ p.video-data { margin: 0; font-weight: bold; font-size: 80%; } /* + * Comments & community posts + */ + +#comments { + max-width: 800px; + margin: auto; +} + +.video-iframe-wrapper { + position: relative; + height: 0; + padding-bottom: 56.25%; +} + +.video-iframe { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + border: none; +} + +/* * Footer */ diff --git a/config/config.example.yml b/config/config.example.yml index 8abe1b9e..7ea80017 100644 --- a/config/config.example.yml +++ b/config/config.example.yml @@ -817,6 +817,16 @@ default_user_preferences: ## Default: true ## #vr_mode: true + + ## + ## Save the playback position + ## Allow to continue watching at the previous position when + ## watching the same video. + ## + ## Accepted values: true, false + ## Default: false + ## + #save_player_pos: false # ----------------------------- # Subscription feed diff --git a/locales/cs.json b/locales/cs.json index 0e8610bf..d9e5b4d5 100644 --- a/locales/cs.json +++ b/locales/cs.json @@ -13,7 +13,7 @@ "Previous page": "Předchozí strana", "Clear watch history?": "Smazat historii?", "New password": "Nové heslo", - "New passwords must match": "Hesla se musí schodovat", + "New passwords must match": "Hesla se musí shodovat", "Cannot change password for Google accounts": "Nelze změnit heslo pro účty Google", "Authorize token?": "Autorizovat token?", "Authorize token for `x`?": "Autorizovat token pro `x`?", diff --git a/locales/es.json b/locales/es.json index 09f510a7..68ff0170 100644 --- a/locales/es.json +++ b/locales/es.json @@ -398,25 +398,24 @@ "search_filters_features_option_three_sixty": "360°", "videoinfo_watch_on_youTube": "Ver en YouTube", "preferences_save_player_pos_label": "Guardar posición de reproducción: ", - "generic_views_count": "{{count}} visualización", - "generic_views_count_plural": "{{count}} visualizaciones", + "generic_views_count": "{{count}} vista", + "generic_views_count_plural": "{{count}} vistas", "generic_subscribers_count": "{{count}} suscriptor", "generic_subscribers_count_plural": "{{count}} suscriptores", "generic_subscriptions_count": "{{count}} suscripción", "generic_subscriptions_count_plural": "{{count}} suscripciones", - "subscriptions_unseen_notifs_count": "{{count}} notificación no vista", - "subscriptions_unseen_notifs_count_plural": "{{count}} notificaciones no vistas", + "subscriptions_unseen_notifs_count": "{{count}} notificación sin ver", + "subscriptions_unseen_notifs_count_plural": "{{count}} notificaciones sin ver", "generic_count_days": "{{count}} día", "generic_count_days_plural": "{{count}} días", "comments_view_x_replies": "Ver {{count}} respuesta", "comments_view_x_replies_plural": "Ver {{count}} respuestas", "generic_count_weeks": "{{count}} semana", "generic_count_weeks_plural": "{{count}} semanas", - "generic_playlists_count": "{{count}} lista de reproducción", - "generic_playlists_count_plural": "{{count}} listas de reproducción", - "generic_videos_count_0": "{{count}} video", - "generic_videos_count_1": "{{count}} videos", - "generic_videos_count_2": "{{count}} videos", + "generic_playlists_count": "{{count}} reproducción", + "generic_playlists_count_plural": "{{count}} reproducciones", + "generic_videos_count": "{{count}} video", + "generic_videos_count_plural": "{{count}} videos", "generic_count_months": "{{count}} mes", "generic_count_months_plural": "{{count}} meses", "comments_points_count": "{{count}} punto", @@ -469,8 +468,8 @@ "search_filters_duration_option_none": "Cualquier duración", "search_filters_features_option_vr180": "VR180", "search_filters_apply_button": "Aplicar filtros", - "tokens_count": "{{count}} ficha", - "tokens_count_plural": "{{count}} fichas", + "tokens_count": "{{count}} token", + "tokens_count_plural": "{{count}} tokens", "search_message_use_another_instance": " También puede <a href=\"`x`\">buscar en otra instancia</a>.", "Popular enabled: ": "¿Habilitar la sección popular? ", "error_video_not_in_playlist": "El video que solicitaste no existe en esta lista de reproducción. <a href=\"`x`\">Haz clic aquí para acceder a la página de inicio de la lista de reproducción.</a>", diff --git a/locales/ko.json b/locales/ko.json index d4f3a711..2b454add 100644 --- a/locales/ko.json +++ b/locales/ko.json @@ -46,7 +46,7 @@ "Log in/register": "로그인/회원가입", "Log in": "로그인", "source": "출처", - "JavaScript license information": "자바스크립트 라이센스 정보", + "JavaScript license information": "자바스크립트 라이선스 정보", "An alternative front-end to YouTube": "유튜브의 프론트엔드 대안", "History": "역사", "Delete account?": "계정을 삭제 하시겠습니까?", @@ -116,7 +116,7 @@ "Show replies": "댓글 보기", "Hide replies": "댓글 숨기기", "Incorrect password": "잘못된 비밀번호", - "License: ": "라이센스: ", + "License: ": "라이선스: ", "Genre: ": "장르: ", "Editing playlist `x`": "재생목록 `x` 수정하기", "Playlist privacy": "재생목록 공개 범위", @@ -135,7 +135,7 @@ "Unlisted": "목록에 없음", "Public": "공개", "View privacy policy.": "개인정보 처리방침 보기.", - "View JavaScript license information.": "자바스크립트 라이센스 정보 보기.", + "View JavaScript license information.": "자바스크립트 라이선스 정보 보기.", "Source available here.": "소스는 여기에서 사용할 수 있습니다.", "Log out": "로그아웃", "search": "검색", @@ -460,5 +460,12 @@ "channel_tab_shorts_label": "쇼츠", "channel_tab_streams_label": "실시간 스트리밍", "channel_tab_channels_label": "채널", - "channel_tab_playlists_label": "재생목록" + "channel_tab_playlists_label": "재생목록", + "Standard YouTube license": "표준 유튜브 라이선스", + "Song: ": "제목: ", + "Channel Sponsor": "채널 스폰서", + "Album: ": "앨범: ", + "Music in this video": "동영상 속 음악", + "Artist: ": "아티스트: ", + "Download is disabled": "다운로드가 비활성화 되어있음" } diff --git a/src/invidious/channels/channels.cr b/src/invidious/channels/channels.cr index 63dd2194..c3d6124f 100644 --- a/src/invidious/channels/channels.cr +++ b/src/invidious/channels/channels.cr @@ -159,12 +159,18 @@ def fetch_channel(ucid, pull_all_videos : Bool) LOGGER.debug("fetch_channel: #{ucid}") LOGGER.trace("fetch_channel: #{ucid} : pull_all_videos = #{pull_all_videos}") + namespaces = { + "yt" => "http://www.youtube.com/xml/schemas/2015", + "media" => "http://search.yahoo.com/mrss/", + "default" => "http://www.w3.org/2005/Atom", + } + LOGGER.trace("fetch_channel: #{ucid} : Downloading RSS feed") rss = YT_POOL.client &.get("/feeds/videos.xml?channel_id=#{ucid}").body LOGGER.trace("fetch_channel: #{ucid} : Parsing RSS feed") - rss = XML.parse_html(rss) + rss = XML.parse(rss) - author = rss.xpath_node(%q(//feed/title)) + author = rss.xpath_node("//default:feed/default:title", namespaces) if !author raise InfoException.new("Deleted or invalid channel") end @@ -192,15 +198,23 @@ def fetch_channel(ucid, pull_all_videos : Bool) videos, continuation = IV::Channel::Tabs.get_videos(channel) LOGGER.trace("fetch_channel: #{ucid} : Extracting videos from channel RSS feed") - rss.xpath_nodes("//feed/entry").each do |entry| - video_id = entry.xpath_node("videoid").not_nil!.content - title = entry.xpath_node("title").not_nil!.content - published = Time.parse_rfc3339(entry.xpath_node("published").not_nil!.content) - updated = Time.parse_rfc3339(entry.xpath_node("updated").not_nil!.content) - author = entry.xpath_node("author/name").not_nil!.content - ucid = entry.xpath_node("channelid").not_nil!.content - views = entry.xpath_node("group/community/statistics").try &.["views"]?.try &.to_i64? - views ||= 0_i64 + rss.xpath_nodes("//default:feed/default:entry", namespaces).each do |entry| + video_id = entry.xpath_node("yt:videoId", namespaces).not_nil!.content + title = entry.xpath_node("default:title", namespaces).not_nil!.content + + published = Time.parse_rfc3339( + entry.xpath_node("default:published", namespaces).not_nil!.content + ) + updated = Time.parse_rfc3339( + entry.xpath_node("default:updated", namespaces).not_nil!.content + ) + + author = entry.xpath_node("default:author/default:name", namespaces).not_nil!.content + ucid = entry.xpath_node("yt:channelId", namespaces).not_nil!.content + + views = entry + .xpath_node("media:group/media:community/media:statistics", namespaces) + .try &.["views"]?.try &.to_i64? || 0_i64 channel_video = videos .select(SearchVideo) diff --git a/src/invidious/comments.cr b/src/invidious/comments.cr index fd2be73d..01556099 100644 --- a/src/invidious/comments.cr +++ b/src/invidious/comments.cr @@ -372,32 +372,25 @@ def template_youtube_comments(comments, locale, thin_mode, is_replies = false) </div> END_HTML when "video" - html << <<-END_HTML - <div class="pure-g"> - <div class="pure-u-1 pure-u-md-1-2"> - <div style="position:relative;width:100%;height:0;padding-bottom:56.25%;margin-bottom:5px"> - END_HTML - if attachment["error"]? html << <<-END_HTML + <div class="pure-g video-iframe-wrapper"> <p>#{attachment["error"]}</p> + </div> END_HTML else html << <<-END_HTML - <iframe id='ivplayer' style='position:absolute;width:100%;height:100%;left:0;top:0' src='/embed/#{attachment["videoId"]?}?autoplay=0' style='border:none;'></iframe> + <div class="pure-g video-iframe-wrapper"> + <iframe class="video-iframe" src='/embed/#{attachment["videoId"]?}?autoplay=0'></iframe> + </div> END_HTML end - - html << <<-END_HTML - </div> - </div> - </div> - END_HTML else nil # Ignore end end html << <<-END_HTML + <p> <span title="#{Time.unix(child["published"].as_i64).to_s(translate(locale, "%A %B %-d, %Y"))}">#{translate(locale, "`x` ago", recode_date(Time.unix(child["published"].as_i64), locale))} #{child["isEdited"] == true ? translate(locale, "(edited)") : ""}</span> | END_HTML @@ -416,6 +409,7 @@ def template_youtube_comments(comments, locale, thin_mode, is_replies = false) html << <<-END_HTML <i class="icon ion-ios-thumbs-up"></i> #{number_with_separator(child["likeCount"])} + </p> END_HTML if child["creatorHeart"]? @@ -604,7 +598,7 @@ def text_to_parsed_content(text : String) : JSON::Any currentNode = {"text" => urlMatch[0], "navigationEndpoint" => {"urlEndpoint" => {"url" => urlMatch[0]}}} currentNodes << (JSON.parse(currentNode.to_json)) # If text remain after match create new simple node with text after match - afterNode = {"text" => splittedLastNode.size > 0 ? splittedLastNode[1] : ""} + afterNode = {"text" => splittedLastNode.size > 1 ? splittedLastNode[1] : ""} currentNodes << (JSON.parse(afterNode.to_json)) end @@ -635,55 +629,8 @@ def content_to_comment_html(content, video_id : String? = "") text = HTML.escape(run["text"].as_s) - if run["navigationEndpoint"]? - if url = run["navigationEndpoint"]["urlEndpoint"]?.try &.["url"].as_s - url = URI.parse(url) - displayed_url = text - - if url.host == "youtu.be" - url = "/watch?v=#{url.request_target.lstrip('/')}" - elsif url.host.nil? || url.host.not_nil!.ends_with?("youtube.com") - if url.path == "/redirect" - # Sometimes, links can be corrupted (why?) so make sure to fallback - # nicely. See https://github.com/iv-org/invidious/issues/2682 - url = url.query_params["q"]? || "" - displayed_url = url - else - url = url.request_target - displayed_url = "youtube.com#{url}" - end - end - - text = %(<a href="#{url}">#{reduce_uri(displayed_url)}</a>) - elsif watch_endpoint = run["navigationEndpoint"]["watchEndpoint"]? - start_time = watch_endpoint["startTimeSeconds"]?.try &.as_i - link_video_id = watch_endpoint["videoId"].as_s - - url = "/watch?v=#{link_video_id}" - url += "&t=#{start_time}" if !start_time.nil? - - # If the current video ID (passed through from the caller function) - # is the same as the video ID in the link, add HTML attributes for - # the JS handler function that bypasses page reload. - # - # See: https://github.com/iv-org/invidious/issues/3063 - if link_video_id == video_id - start_time ||= 0 - text = %(<a href="#{url}" data-onclick="jump_to_time" data-jump-time="#{start_time}">#{reduce_uri(text)}</a>) - else - text = %(<a href="#{url}">#{text}</a>) - end - elsif url = run.dig?("navigationEndpoint", "commandMetadata", "webCommandMetadata", "url").try &.as_s - if text.starts_with?(/\s?[@#]/) - # Handle "pings" in comments and hasthags differently - # See: - # - https://github.com/iv-org/invidious/issues/3038 - # - https://github.com/iv-org/invidious/issues/3062 - text = %(<a href="#{url}">#{text}</a>) - else - text = %(<a href="#{url}">#{reduce_uri(url)}</a>) - end - end + if navigationEndpoint = run.dig?("navigationEndpoint") + text = parse_link_endpoint(navigationEndpoint, text, video_id) end text = "<b>#{text}</b>" if run["bold"]? diff --git a/src/invidious/database/users.cr b/src/invidious/database/users.cr index 0a4a4fd8..d54e6a76 100644 --- a/src/invidious/database/users.cr +++ b/src/invidious/database/users.cr @@ -52,7 +52,7 @@ module Invidious::Database::Users def mark_watched(user : User, vid : String) request = <<-SQL UPDATE users - SET watched = array_append(watched, $1) + SET watched = array_append(array_remove(watched, $1), $1) WHERE email = $2 SQL diff --git a/src/invidious/helpers/utils.cr b/src/invidious/helpers/utils.cr index 500a2582..bcf7c963 100644 --- a/src/invidious/helpers/utils.cr +++ b/src/invidious/helpers/utils.cr @@ -389,3 +389,56 @@ def reduce_uri(uri : URI | String, max_length : Int32 = 50, suffix : String = " end return str end + +# Get the html link from a NavigationEndpoint or an innertubeCommand +def parse_link_endpoint(endpoint : JSON::Any, text : String, video_id : String) + if url = endpoint.dig?("urlEndpoint", "url").try &.as_s + url = URI.parse(url) + displayed_url = text + + if url.host == "youtu.be" + url = "/watch?v=#{url.request_target.lstrip('/')}" + elsif url.host.nil? || url.host.not_nil!.ends_with?("youtube.com") + if url.path == "/redirect" + # Sometimes, links can be corrupted (why?) so make sure to fallback + # nicely. See https://github.com/iv-org/invidious/issues/2682 + url = url.query_params["q"]? || "" + displayed_url = url + else + url = url.request_target + displayed_url = "youtube.com#{url}" + end + end + + text = %(<a href="#{url}">#{reduce_uri(displayed_url)}</a>) + elsif watch_endpoint = endpoint.dig?("watchEndpoint") + start_time = watch_endpoint["startTimeSeconds"]?.try &.as_i + link_video_id = watch_endpoint["videoId"].as_s + + url = "/watch?v=#{link_video_id}" + url += "&t=#{start_time}" if !start_time.nil? + + # If the current video ID (passed through from the caller function) + # is the same as the video ID in the link, add HTML attributes for + # the JS handler function that bypasses page reload. + # + # See: https://github.com/iv-org/invidious/issues/3063 + if link_video_id == video_id + start_time ||= 0 + text = %(<a href="#{url}" data-onclick="jump_to_time" data-jump-time="#{start_time}">#{reduce_uri(text)}</a>) + else + text = %(<a href="#{url}">#{text}</a>) + end + elsif url = endpoint.dig?("commandMetadata", "webCommandMetadata", "url").try &.as_s + if text.starts_with?(/\s?[@#]/) + # Handle "pings" in comments and hasthags differently + # See: + # - https://github.com/iv-org/invidious/issues/3038 + # - https://github.com/iv-org/invidious/issues/3062 + text = %(<a href="#{url}">#{text}</a>) + else + text = %(<a href="#{url}">#{reduce_uri(url)}</a>) + end + end + return text +end diff --git a/src/invidious/routes/watch.cr b/src/invidious/routes/watch.cr index 5d3845c3..813cb0f4 100644 --- a/src/invidious/routes/watch.cr +++ b/src/invidious/routes/watch.cr @@ -76,7 +76,7 @@ module Invidious::Routes::Watch end env.params.query.delete_all("iv_load_policy") - if watched && preferences.watch_history && !watched.includes? id + if watched && preferences.watch_history Invidious::Database::Users.mark_watched(user.as(User), id) end @@ -259,9 +259,7 @@ module Invidious::Routes::Watch case action when "action_mark_watched" - if !user.watched.includes? id - Invidious::Database::Users.mark_watched(user, id) - end + Invidious::Database::Users.mark_watched(user, id) when "action_mark_unwatched" Invidious::Database::Users.mark_unwatched(user, id) else diff --git a/src/invidious/trending.cr b/src/invidious/trending.cr index 74bab1bd..2d9f8a83 100644 --- a/src/invidious/trending.cr +++ b/src/invidious/trending.cr @@ -20,6 +20,21 @@ def fetch_trending(trending_type, region, locale) items, _ = extract_items(initial_data) - # Return items, but ignore categories (e.g featured content) - return items.reject!(Category), plid + extracted = [] of SearchItem + + items.each do |itm| + if itm.is_a?(Category) + # Ignore the smaller categories, as they generally contain a sponsored + # channel, which brings a lot of noise on the trending page. + # See: https://github.com/iv-org/invidious/issues/2989 + next if itm.contents.size < 24 + + extracted.concat extract_category(itm) + else + extracted << itm + end + end + + # Deduplicate items before returning results + return extracted.select(SearchVideo).uniq!(&.id), plid end diff --git a/src/invidious/user/imports.cr b/src/invidious/user/imports.cr index 20ae0d47..aa947456 100644 --- a/src/invidious/user/imports.cr +++ b/src/invidious/user/imports.cr @@ -48,7 +48,7 @@ struct Invidious::User if data["watch_history"]? user.watched += data["watch_history"].as_a.map(&.as_s) - user.watched.uniq! + user.watched.reverse!.uniq!.reverse! Invidious::Database::Users.update_watch_history(user) end diff --git a/src/invidious/videos/description.cr b/src/invidious/videos/description.cr index 2017955d..542cb416 100644 --- a/src/invidious/videos/description.cr +++ b/src/invidious/videos/description.cr @@ -1,51 +1,6 @@ require "json" require "uri" -def parse_command(command : JSON::Any?, string : String) : String? - on_tap = command.dig?("onTap", "innertubeCommand") - - # 3rd party URL, extract original URL from YouTube tracking URL - if url_endpoint = on_tap.try &.["urlEndpoint"]? - youtube_url = URI.parse url_endpoint["url"].as_s - - original_url = youtube_url.query_params["q"]? - if original_url.nil? - return "" - else - return "<a href=\"#{original_url}\">#{original_url}</a>" - end - # 1st party watch URL - elsif watch_endpoint = on_tap.try &.["watchEndpoint"]? - video_id = watch_endpoint["videoId"].as_s - time = watch_endpoint["startTimeSeconds"].as_i - - url = "/watch?v=#{video_id}&t=#{time}s" - - # if string is a timestamp, use the string instead - # this is a lazy regex for validating timestamps - if /(?:\d{1,2}:){1,2}\d{2}/ =~ string - return "<a href=\"#{url}\">#{string}</a>" - else - return "<a href=\"#{url}\">#{url}</a>" - end - # hashtag/other browse URLs - elsif browse_endpoint = on_tap.try &.dig?("commandMetadata", "webCommandMetadata") - url = browse_endpoint["url"].try &.as_s - - # remove unnecessary character in a channel name - if browse_endpoint["webPageType"]?.try &.as_s == "WEB_PAGE_TYPE_CHANNEL" - name = string.match(/@[\w\d.-]+/) - if name.try &.[0]? - return "<a href=\"#{url}\">#{name.try &.[0]}</a>" - end - end - - return "<a href=\"#{url}\">#{string}</a>" - end - - return "(unknown YouTube desc command)" -end - private def copy_string(str : String::Builder, iter : Iterator, count : Int) : Int copied = 0 while copied < count @@ -62,7 +17,7 @@ private def copy_string(str : String::Builder, iter : Iterator, count : Int) : I return copied end -def parse_description(desc : JSON::Any?) : String? +def parse_description(desc, video_id : String) : String? return "" if desc.nil? content = desc["content"].as_s @@ -94,7 +49,11 @@ def parse_description(desc : JSON::Any?) : String? copy_string(str2, iter, cmd_length) end - str << parse_command(command, cmd_content) + link = cmd_content + if on_tap = command.dig?("onTap", "innertubeCommand") + link = parse_link_endpoint(on_tap, cmd_content, video_id) + end + str << link index += cmd_length end diff --git a/src/invidious/videos/parser.cr b/src/invidious/videos/parser.cr index 1c6d118d..2e8eecc3 100644 --- a/src/invidious/videos/parser.cr +++ b/src/invidious/videos/parser.cr @@ -287,7 +287,7 @@ def parse_video_info(video_id : String, player_response : Hash(String, JSON::Any # description_html = video_secondary_renderer.try &.dig?("description", "runs") # .try &.as_a.try { |t| content_to_comment_html(t, video_id) } - description_html = parse_description(video_secondary_renderer.try &.dig?("attributedDescription")) + description_html = parse_description(video_secondary_renderer.try &.dig?("attributedDescription"), video_id) # Video metadata diff --git a/src/invidious/yt_backend/extractors_utils.cr b/src/invidious/yt_backend/extractors_utils.cr index b247dca8..11d95958 100644 --- a/src/invidious/yt_backend/extractors_utils.cr +++ b/src/invidious/yt_backend/extractors_utils.cr @@ -68,16 +68,17 @@ rescue ex return false end -# This function extracts the SearchItems from a Category. +# This function extracts SearchVideo items from a Category. # Categories are commonly returned in search results and trending pages. def extract_category(category : Category) : Array(SearchVideo) - items = [] of SearchItem + return category.contents.select(SearchVideo) +end - category.contents.each do |item| - target << cate_i if item.is_a?(SearchItem) +# :ditto: +def extract_category(category : Category, &) + category.contents.select(SearchVideo).each do |item| + yield item end - - return items end def extract_selected_tab(tabs) |
