Merge branch 'master' of github.com:iv-org/invidious
このコミットが含まれているのは:
コミット
5bbd2be78d
|
@ -38,10 +38,10 @@ jobs:
|
|||
matrix:
|
||||
stable: [true]
|
||||
crystal:
|
||||
- 1.2.2
|
||||
- 1.3.2
|
||||
- 1.4.0
|
||||
- 1.5.0
|
||||
- 1.4.1
|
||||
- 1.5.1
|
||||
- 1.6.1
|
||||
include:
|
||||
- crystal: nightly
|
||||
stable: false
|
||||
|
@ -52,7 +52,7 @@ jobs:
|
|||
submodules: true
|
||||
|
||||
- name: Install Crystal
|
||||
uses: crystal-lang/install-crystal@v1.6.0
|
||||
uses: crystal-lang/install-crystal@v1.7.0
|
||||
with:
|
||||
crystal: ${{ matrix.crystal }}
|
||||
|
||||
|
|
|
@ -52,7 +52,7 @@ jobs:
|
|||
username: ${{ secrets.QUAY_USERNAME }}
|
||||
password: ${{ secrets.QUAY_PASSWORD }}
|
||||
|
||||
- name: Build and push Docker AMD64 image for Push Event
|
||||
- name: Build and push Docker AMD64 image without QUIC for Push Event
|
||||
if: github.ref == 'refs/heads/master'
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
|
@ -62,9 +62,11 @@ jobs:
|
|||
labels: quay.expires-after=12w
|
||||
push: true
|
||||
tags: quay.io/invidious/invidious:${{ github.sha }},quay.io/invidious/invidious:latest
|
||||
build-args: release=1
|
||||
build-args: |
|
||||
"release=1"
|
||||
"disable_quic=1"
|
||||
|
||||
- name: Build and push Docker ARM64 image for Push Event
|
||||
- name: Build and push Docker ARM64 image without QUIC for Push Event
|
||||
if: github.ref == 'refs/heads/master'
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
|
@ -74,4 +76,30 @@ jobs:
|
|||
labels: quay.expires-after=12w
|
||||
push: true
|
||||
tags: quay.io/invidious/invidious:${{ github.sha }}-arm64,quay.io/invidious/invidious:latest-arm64
|
||||
build-args: |
|
||||
"release=1"
|
||||
"disable_quic=1"
|
||||
|
||||
- name: Build and push Docker AMD64 image with QUIC for Push Event
|
||||
if: github.ref == 'refs/heads/master'
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: .
|
||||
file: docker/Dockerfile
|
||||
platforms: linux/amd64
|
||||
labels: quay.expires-after=12w
|
||||
push: true
|
||||
tags: quay.io/invidious/invidious:${{ github.sha }}-quic,quay.io/invidious/invidious:latest-quic
|
||||
build-args: release=1
|
||||
|
||||
- name: Build and push Docker ARM64 image with QUIC for Push Event
|
||||
if: github.ref == 'refs/heads/master'
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: .
|
||||
file: docker/Dockerfile.arm64
|
||||
platforms: linux/arm64/v8
|
||||
labels: quay.expires-after=12w
|
||||
push: true
|
||||
tags: quay.io/invidious/invidious:${{ github.sha }}-arm64-quic,quay.io/invidious/invidious:latest-arm64-quic
|
||||
build-args: release=1
|
||||
|
|
2
Makefile
2
Makefile
|
@ -5,7 +5,7 @@
|
|||
RELEASE := 1
|
||||
STATIC := 0
|
||||
|
||||
DISABLE_QUIC := 0
|
||||
DISABLE_QUIC := 1
|
||||
NO_DBG_SYMBOLS := 0
|
||||
|
||||
|
||||
|
|
|
@ -304,10 +304,8 @@ https_only: false
|
|||
## Number of threads to use when crawling channel videos (during
|
||||
## subscriptions update).
|
||||
##
|
||||
## Notes:
|
||||
## - Setting this to 0 will disable the channel videos crawl job.
|
||||
## - This setting is overridden if "-c THREADS" or
|
||||
## "--channel-threads=THREADS" are passed on the command line.
|
||||
## Notes: This setting is overridden if either "-c THREADS" or
|
||||
## "--channel-threads=THREADS" is passed on the command line.
|
||||
##
|
||||
## Accepted values: a positive integer
|
||||
## Default: 1
|
||||
|
@ -335,10 +333,8 @@ full_refresh: false
|
|||
##
|
||||
## Number of threads to use when updating RSS feeds.
|
||||
##
|
||||
## Notes:
|
||||
## - Setting this to 0 will disable the channel videos crawl job.
|
||||
## - This setting is overridden if "-f THREADS" or
|
||||
## "--feed-threads=THREADS" are passed on the command line.
|
||||
## Notes: This setting is overridden if either "-f THREADS" or
|
||||
## "--feed-threads=THREADS" is passed on the command line.
|
||||
##
|
||||
## Accepted values: a positive integer
|
||||
## Default: 1
|
||||
|
@ -361,6 +357,39 @@ feed_threads: 1
|
|||
#decrypt_polling: false
|
||||
|
||||
|
||||
jobs:
|
||||
|
||||
## Options for the database cleaning job
|
||||
clear_expired_items:
|
||||
|
||||
## Enable/Disable job
|
||||
##
|
||||
## Accepted values: true, false
|
||||
## Default: true
|
||||
##
|
||||
enable: true
|
||||
|
||||
## Options for the channels updater job
|
||||
refresh_channels:
|
||||
|
||||
## Enable/Disable job
|
||||
##
|
||||
## Accepted values: true, false
|
||||
## Default: true
|
||||
##
|
||||
enable: true
|
||||
|
||||
## Options for the RSS feeds updater job
|
||||
refresh_feeds:
|
||||
|
||||
## Enable/Disable job
|
||||
##
|
||||
## Accepted values: true, false
|
||||
## Default: true
|
||||
##
|
||||
enable: true
|
||||
|
||||
|
||||
# -----------------------------
|
||||
# Captcha API
|
||||
# -----------------------------
|
||||
|
|
|
@ -2,6 +2,7 @@ FROM crystallang/crystal:1.4.1-alpine AS builder
|
|||
RUN apk add --no-cache sqlite-static yaml-static
|
||||
|
||||
ARG release
|
||||
ARG disable_quic
|
||||
|
||||
WORKDIR /invidious
|
||||
COPY ./shard.yml ./shard.yml
|
||||
|
@ -23,7 +24,13 @@ COPY ./videojs-dependencies.yml ./videojs-dependencies.yml
|
|||
RUN crystal spec --warnings all \
|
||||
--link-flags "-lxml2 -llzma"
|
||||
|
||||
RUN if [ "${release}" == 1 ] ; then \
|
||||
RUN if [[ "${release}" == 1 && "${disable_quic}" == 1 ]] ; then \
|
||||
crystal build ./src/invidious.cr \
|
||||
--release \
|
||||
-Ddisable_quic \
|
||||
--static --warnings all \
|
||||
--link-flags "-lxml2 -llzma"; \
|
||||
elif [[ "${release}" == 1 ]] ; then \
|
||||
crystal build ./src/invidious.cr \
|
||||
--release \
|
||||
--static --warnings all \
|
||||
|
@ -35,7 +42,7 @@ RUN if [ "${release}" == 1 ] ; then \
|
|||
fi
|
||||
|
||||
|
||||
FROM alpine:latest
|
||||
FROM alpine:3.16
|
||||
RUN apk add --no-cache librsvg ttf-opensans
|
||||
WORKDIR /invidious
|
||||
RUN addgroup -g 1000 -S invidious && \
|
||||
|
|
|
@ -2,6 +2,7 @@ FROM alpine:3.16 AS builder
|
|||
RUN apk add --no-cache 'crystal=1.4.1-r0' shards sqlite-static yaml-static yaml-dev libxml2-dev zlib-static openssl-libs-static openssl-dev musl-dev
|
||||
|
||||
ARG release
|
||||
ARG disable_quic
|
||||
|
||||
WORKDIR /invidious
|
||||
COPY ./shard.yml ./shard.yml
|
||||
|
@ -23,7 +24,13 @@ COPY ./videojs-dependencies.yml ./videojs-dependencies.yml
|
|||
RUN crystal spec --warnings all \
|
||||
--link-flags "-lxml2 -llzma"
|
||||
|
||||
RUN if [ ${release} == 1 ] ; then \
|
||||
RUN if [[ "${release}" == 1 && "${disable_quic}" == 1 ]] ; then \
|
||||
crystal build ./src/invidious.cr \
|
||||
--release \
|
||||
-Ddisable_quic \
|
||||
--static --warnings all \
|
||||
--link-flags "-lxml2 -llzma"; \
|
||||
elif [[ "${release}" == 1 ]] ; then \
|
||||
crystal build ./src/invidious.cr \
|
||||
--release \
|
||||
--static --warnings all \
|
||||
|
|
|
@ -34,8 +34,6 @@ securityContext:
|
|||
|
||||
# See https://github.com/bitnami/charts/tree/master/bitnami/postgresql
|
||||
postgresql:
|
||||
image:
|
||||
registry: quay.io
|
||||
auth:
|
||||
username: kemal
|
||||
password: kemal
|
||||
|
|
118
locales/eo.json
118
locales/eo.json
|
@ -21,15 +21,15 @@
|
|||
"No": "Ne",
|
||||
"Import and Export Data": "Importi kaj Eksporti Datumojn",
|
||||
"Import": "Importi",
|
||||
"Import Invidious data": "Importi datumojn de Invidious",
|
||||
"Import YouTube subscriptions": "Importi abonojn de JuTubo",
|
||||
"Import Invidious data": "Importi JSON-datumojn de Invidious",
|
||||
"Import YouTube subscriptions": "Importi abonojn de YouTube/OPML",
|
||||
"Import FreeTube subscriptions (.db)": "Importi abonojn de FreeTube (.db)",
|
||||
"Import NewPipe subscriptions (.json)": "Importi abonojn de NewPipe (.json)",
|
||||
"Import NewPipe data (.zip)": "Importi datumojn de NewPipe (.zip)",
|
||||
"Export": "Eksporti",
|
||||
"Export subscriptions as OPML": "Eksporti abonojn kiel OPML",
|
||||
"Export subscriptions as OPML (for NewPipe & FreeTube)": "Eksporti abonojn kiel OPML (por NewPipe kaj FreeTube)",
|
||||
"Export data as JSON": "Eksporti datumojn kiel JSON",
|
||||
"Export data as JSON": "Eksporti Invidious-datumojn kiel JSON",
|
||||
"Delete account?": "Ĉu forigi konton?",
|
||||
"History": "Historio",
|
||||
"An alternative front-end to YouTube": "Alternativa fasado al JuTubo",
|
||||
|
@ -66,7 +66,7 @@
|
|||
"preferences_related_videos_label": "Ĉu montri rilatajn filmetojn? ",
|
||||
"preferences_annotations_label": "Ĉu montri prinotojn defaŭlte? ",
|
||||
"preferences_extend_desc_label": "Aŭtomate etendi priskribon de filmeto: ",
|
||||
"preferences_vr_mode_label": "Interagaj 360-gradaj filmetoj: ",
|
||||
"preferences_vr_mode_label": "Interagaj 360-gradaj filmoj (postulas WebGL-n): ",
|
||||
"preferences_category_visual": "Vidaj preferoj",
|
||||
"preferences_player_style_label": "Ludila stilo: ",
|
||||
"Dark mode: ": "Malhela reĝimo: ",
|
||||
|
@ -75,7 +75,7 @@
|
|||
"light": "hela",
|
||||
"preferences_thin_mode_label": "Maldika reĝimo: ",
|
||||
"preferences_category_misc": "Aliaj agordoj",
|
||||
"preferences_automatic_instance_redirect_label": "Aŭtomata alidirektado de instalaĵo (retropaŝo al redirect.invidious.io): ",
|
||||
"preferences_automatic_instance_redirect_label": "Aŭtomata alidirektado de nodo (retropaŝo al redirect.invidious.io): ",
|
||||
"preferences_category_subscription": "Abonaj agordoj",
|
||||
"preferences_annotations_subscribed_label": "Ĉu montri prinotojn defaŭlte por abonitaj kanaloj? ",
|
||||
"Redirect homepage to feed: ": "Alidirekti hejmpâgon al fluo: ",
|
||||
|
@ -140,7 +140,7 @@
|
|||
"Show more": "Montri pli",
|
||||
"Show less": "Montri malpli",
|
||||
"Watch on YouTube": "Vidi filmeton en JuTubo",
|
||||
"Switch Invidious Instance": "Ŝanĝi instalaĵon de Indivious",
|
||||
"Switch Invidious Instance": "Ŝanĝi nodon de Indivious",
|
||||
"Hide annotations": "Kaŝi prinotojn",
|
||||
"Show annotations": "Montri prinotojn",
|
||||
"Genre: ": "Ĝenro: ",
|
||||
|
@ -368,5 +368,109 @@
|
|||
"footer_donate_page": "Donaci",
|
||||
"preferences_region_label": "Lando de la enhavo: ",
|
||||
"preferences_quality_dash_label": "Preferata DASH-a videkvalito: ",
|
||||
"search_filters_title": "Filtri"
|
||||
"search_filters_title": "Filtri",
|
||||
"preferences_quality_dash_option_best": "Plej bona",
|
||||
"preferences_quality_dash_option_worst": "Malplej bona",
|
||||
"Popular enabled: ": "Populara sekcio ebligita: ",
|
||||
"search_message_no_results": "Neniu rezulto trovita.",
|
||||
"search_message_use_another_instance": " Vi ankaŭ povas <a href=\"`x`\">serĉi en alia nodo</a>.",
|
||||
"tokens_count": "{{count}} ĵetono",
|
||||
"tokens_count_plural": "{{count}} ĵetonoj",
|
||||
"subscriptions_unseen_notifs_count": "{{count}} nevidita sciigo",
|
||||
"subscriptions_unseen_notifs_count_plural": "{{count}} neviditaj sciigoj",
|
||||
"Indonesian (auto-generated)": "Indonezia (aŭtomate generita)",
|
||||
"Interlingue": "Interlingvo",
|
||||
"Italian (auto-generated)": "Itala (aŭtomate generita)",
|
||||
"Korean (auto-generated)": "Korea (aŭtomate generita)",
|
||||
"Portuguese (Brazil)": "Portugala (Brazilo)",
|
||||
"Portuguese (auto-generated)": "Portugala (aŭtomate generita)",
|
||||
"Russian (auto-generated)": "Rusa (aŭtomate generita)",
|
||||
"Spanish (Spain)": "Hispana (Hispanio)",
|
||||
"generic_count_years": "{{count}} jaro",
|
||||
"generic_count_years_plural": "{{count}} jaroj",
|
||||
"Turkish (auto-generated)": "Turka (aŭtomate generita)",
|
||||
"Vietnamese (auto-generated)": "Vjetnama (aŭtomate generita)",
|
||||
"generic_count_hours": "{{count}} horo",
|
||||
"generic_count_hours_plural": "{{count}} horoj",
|
||||
"generic_count_minutes": "{{count}} minuto",
|
||||
"generic_count_minutes_plural": "{{count}} minutoj",
|
||||
"search_filters_date_label": "Alŝutdato",
|
||||
"search_filters_date_option_none": "Ajna dato",
|
||||
"search_filters_duration_option_medium": "Meza (4 - 20 minutoj)",
|
||||
"search_filters_features_option_three_sixty": "360º",
|
||||
"search_filters_features_option_vr180": "VR180",
|
||||
"user_created_playlists": "`x`kreitaj ludlistoj",
|
||||
"user_saved_playlists": "`x`konservitaj ludlistoj",
|
||||
"crash_page_switch_instance": "klopodis <a href=\"`x`\">uzi alian nodon</a>",
|
||||
"crash_page_read_the_faq": "legis la <a href=\"`x`\">oftajn demandojn</a>",
|
||||
"error_video_not_in_playlist": "La petita video ne ekzistas en ĉi tiu ludlisto. <a href=\"`x`\">Alklaku ĉi tie por iri al la ludlista hejmpaĝo.</a>",
|
||||
"crash_page_search_issue": "serĉis por <a href=\"`x`\">ekzistantaj problemoj en GitHub</a>",
|
||||
"generic_count_seconds": "{{count}} sekundo",
|
||||
"generic_count_seconds_plural": "{{count}} sekundoj",
|
||||
"preferences_quality_dash_option_144p": "144p",
|
||||
"comments_view_x_replies": "Vidi {{count}} respondon",
|
||||
"comments_view_x_replies_plural": "Vidi {{count}} respondojn",
|
||||
"preferences_quality_dash_option_360p": "360p",
|
||||
"invidious": "Invidious",
|
||||
"Chinese (Taiwan)": "Ĉina (Tajvano)",
|
||||
"English (United Kingdom)": "Angla (Britio)",
|
||||
"search_filters_features_option_purchased": "Aĉetita",
|
||||
"Japanese (auto-generated)": "Japana (aŭtomate generita)",
|
||||
"search_message_change_filters_or_query": "Provu vastigi vian serĉpeton kaj/aŭ ŝanĝi la filtrilojn.",
|
||||
"preferences_quality_dash_option_1080p": "1080p",
|
||||
"generic_count_weeks": "{{count}} semajno",
|
||||
"generic_count_weeks_plural": "{{count}} semajnoj",
|
||||
"preferences_quality_dash_option_240p": "240p",
|
||||
"preferences_quality_dash_option_1440p": "1440p",
|
||||
"preferences_quality_dash_option_4320p": "4320p",
|
||||
"preferences_quality_dash_option_720p": "720p",
|
||||
"preferences_quality_dash_option_auto": "Aŭtomate",
|
||||
"preferences_quality_dash_option_2160p": "2160p",
|
||||
"English (United States)": "Angla (Usono)",
|
||||
"Chinese": "Ĉina",
|
||||
"videoinfo_watch_on_youTube": "Vidi en YouTube",
|
||||
"crash_page_you_found_a_bug": "Ŝajnas, ke vi trovis eraron en Invidious!",
|
||||
"comments_points_count": "{{count}} poento",
|
||||
"comments_points_count_plural": "{{count}} poentoj",
|
||||
"Cantonese (Hong Kong)": "Kantona (Honkongo)",
|
||||
"preferences_watch_history_label": "Ebligi vidohistorion: ",
|
||||
"preferences_quality_option_small": "Eta",
|
||||
"generic_playlists_count": "{{count}} ludlisto",
|
||||
"generic_playlists_count_plural": "{{count}} ludlistoj",
|
||||
"videoinfo_youTube_embed_link": "Enigi",
|
||||
"preferences_quality_dash_option_480p": "480p",
|
||||
"preferences_quality_option_hd720": "HD720",
|
||||
"preferences_quality_option_medium": "Meza",
|
||||
"generic_subscriptions_count": "{{count}} abono",
|
||||
"generic_subscriptions_count_plural": "{{count}} abonoj",
|
||||
"videoinfo_started_streaming_x_ago": "Komercis elsendi antaŭ `x`",
|
||||
"download_subtitles": "Subtitoloj - `x` (.vtt)",
|
||||
"videoinfo_invidious_embed_link": "Enigi Ligilon",
|
||||
"crash_page_report_issue": "Se neniu el la antaŭaj agoj helpis, bonvolu <a href=\"`x`\">estigi novan problemon en GitHub</a> (prefere angle) kaj inkludi la jenan tekston en via mesaĝo (NE traduku tiun tekston):",
|
||||
"preferences_quality_option_dash": "DASH (adapta kvalito)",
|
||||
"Chinese (Hong Kong)": "Ĉina (Honkongo)",
|
||||
"Chinese (China)": "Ĉina (Ĉinio)",
|
||||
"Dutch (auto-generated)": "Nederlanda (aŭtomate generita)",
|
||||
"German (auto-generated)": "Germana (aŭtomate generita)",
|
||||
"French (auto-generated)": "Franca (aŭtomate generita)",
|
||||
"Spanish (Mexico)": "Hispana (Meksiko)",
|
||||
"Spanish (auto-generated)": "Hispana (aŭtomate generita)",
|
||||
"generic_count_days": "{{count}} jaro",
|
||||
"generic_count_days_plural": "{{count}} jaroj",
|
||||
"search_filters_type_option_all": "Ajna speco",
|
||||
"search_filters_duration_option_none": "Ajna daŭro",
|
||||
"search_filters_apply_button": "Uzi elektitajn filtrilojn",
|
||||
"none": "neniu",
|
||||
"Video unavailable": "Nedisponebla video",
|
||||
"crash_page_before_reporting": "Antaŭ ol informi pri eraro certigu, ke vi:",
|
||||
"crash_page_refresh": "klopodis <a href=\"`x`\">reŝarĝi la paĝon</a>",
|
||||
"generic_views_count": "{{count}} spekto",
|
||||
"generic_views_count_plural": "{{count}} spektoj",
|
||||
"generic_videos_count": "{{count}} video",
|
||||
"generic_videos_count_plural": "{{count}} videoj",
|
||||
"generic_subscribers_count": "{{count}} abonanto",
|
||||
"generic_subscribers_count_plural": "{{count}} abonantoj",
|
||||
"generic_count_months": "{{count}} monato",
|
||||
"generic_count_months_plural": "{{count}} monatoj",
|
||||
"preferences_save_player_pos_label": "Konservi ludadan pozicion: "
|
||||
}
|
||||
|
|
|
@ -114,7 +114,7 @@
|
|||
"Save preferences": "Guardar las preferencias",
|
||||
"Subscription manager": "Gestor de suscripciones",
|
||||
"Token manager": "Gestor de tokens",
|
||||
"Token": "Token",
|
||||
"Token": "Ficha",
|
||||
"Import/export": "Importar/Exportar",
|
||||
"unsubscribe": "Desuscribirse",
|
||||
"revoke": "revocar",
|
||||
|
@ -355,7 +355,7 @@
|
|||
"search_filters_features_option_location": "ubicación",
|
||||
"search_filters_features_option_hdr": "hdr",
|
||||
"Current version: ": "Versión actual: ",
|
||||
"next_steps_error_message": "Después de lo cual deberías intentar: ",
|
||||
"next_steps_error_message": "Después de lo cual debes intentar: ",
|
||||
"next_steps_error_message_refresh": "Recargar la página",
|
||||
"next_steps_error_message_go_to_youtube": "Ir a YouTube",
|
||||
"search_filters_duration_option_short": "Corto (< 4 minutos)",
|
||||
|
@ -467,8 +467,8 @@
|
|||
"search_filters_duration_option_none": "Cualquier duración",
|
||||
"search_filters_features_option_vr180": "VR180",
|
||||
"search_filters_apply_button": "Aplicar filtros seleccionados",
|
||||
"tokens_count": "{{count}} token",
|
||||
"tokens_count_plural": "{{count}} tokens",
|
||||
"tokens_count": "{{count}} ficha",
|
||||
"tokens_count_plural": "{{count}} fichas",
|
||||
"search_message_use_another_instance": " También puede <a href=\"`x`\">buscar en otra instancia</a>.",
|
||||
"search_filters_duration_option_medium": "Medio (4 - 20 minutes)",
|
||||
"Popular enabled: ": "¿Habilitar la sección popular? ",
|
||||
|
|
103
locales/ko.json
103
locales/ko.json
|
@ -12,22 +12,22 @@
|
|||
"Dark mode: ": "다크 모드: ",
|
||||
"preferences_player_style_label": "플레이어 스타일: ",
|
||||
"preferences_category_visual": "시각 설정",
|
||||
"preferences_vr_mode_label": "360도 비디오와 상호작용하기(WebGL를 요구함): ",
|
||||
"preferences_vr_mode_label": "VR 영상 활성화(WebGL 필요): ",
|
||||
"preferences_extend_desc_label": "자동으로 비디오 설명을 확장: ",
|
||||
"preferences_annotations_label": "기본적으로 주석 표시: ",
|
||||
"preferences_annotations_label": "기본으로 주석 표시: ",
|
||||
"preferences_related_videos_label": "관련 동영상 보기: ",
|
||||
"Fallback captions: ": "대체 자막: ",
|
||||
"preferences_captions_label": "기본 자막: ",
|
||||
"reddit": "Reddit",
|
||||
"youtube": "YouTube",
|
||||
"reddit": "레딧",
|
||||
"youtube": "유튜브",
|
||||
"preferences_comments_label": "기본 댓글: ",
|
||||
"preferences_volume_label": "플레이어 볼륨: ",
|
||||
"preferences_quality_label": "선호하는 비디오 품질: ",
|
||||
"preferences_speed_label": "기본 속도: ",
|
||||
"preferences_local_label": "비디오를 프록시: ",
|
||||
"preferences_listen_label": "기본적으로 듣기: ",
|
||||
"preferences_listen_label": "라디오 모드 활성화: ",
|
||||
"preferences_continue_autoplay_label": "다음 동영상 자동재생 ",
|
||||
"preferences_continue_label": "기본적으로 다음 재생: ",
|
||||
"preferences_continue_label": "다음 동영상으로 이동: ",
|
||||
"preferences_autoplay_label": "자동재생: ",
|
||||
"preferences_video_loop_label": "항상 반복: ",
|
||||
"preferences_category_player": "플레이어 설정",
|
||||
|
@ -46,8 +46,8 @@
|
|||
"Log in/register": "로그인/회원가입",
|
||||
"Log in": "로그인",
|
||||
"source": "출처",
|
||||
"JavaScript license information": "JavaScript 라이선스 정보",
|
||||
"An alternative front-end to YouTube": "YouTube의 대안 프론트엔드",
|
||||
"JavaScript license information": "자바스크립트 라이센스 정보",
|
||||
"An alternative front-end to YouTube": "유튜브의 프론트엔드 대안",
|
||||
"History": "역사",
|
||||
"Delete account?": "계정을 삭제 하시겠습니까?",
|
||||
"Export data as JSON": "데이터를 JSON으로 내보내기",
|
||||
|
@ -57,15 +57,15 @@
|
|||
"Import NewPipe data (.zip)": "NewPipe 데이터 가져오기 (.zip)",
|
||||
"Import NewPipe subscriptions (.json)": "NewPipe 구독을 가져오기 (.json)",
|
||||
"Import FreeTube subscriptions (.db)": "FreeTube 구독 가져오기 (.db)",
|
||||
"Import YouTube subscriptions": "YouTube 구독 가져오기",
|
||||
"Import Invidious data": "Invidious JSON 데이터 가져오기",
|
||||
"Import YouTube subscriptions": "유튜브 구독 가져오기",
|
||||
"Import Invidious data": "인비디어스 JSON 데이터 가져오기",
|
||||
"Import": "가져오기",
|
||||
"Import and Export Data": "데이터 가져오기 및 내보내기",
|
||||
"No": "아니요",
|
||||
"Yes": "예",
|
||||
"Authorize token for `x`?": "`x` 에 대한 토큰을 승인하시겠습니까?",
|
||||
"Authorize token?": "토큰을 승인하시겠습니까?",
|
||||
"Cannot change password for Google accounts": "Google 계정의 비밀번호를 변경할 수 없습니다",
|
||||
"Cannot change password for Google accounts": "구글 계정의 비밀번호를 변경할 수 없습니다",
|
||||
"New passwords must match": "새 비밀번호는 일치해야 합니다",
|
||||
"New password": "새 비밀번호",
|
||||
"Clear watch history?": "재생 기록을 삭제 하시겠습니까?",
|
||||
|
@ -76,8 +76,8 @@
|
|||
"popular": "인기",
|
||||
"oldest": "오래된순",
|
||||
"newest": "최신순",
|
||||
"View playlist on YouTube": "YouTube에서 재생목록 보기",
|
||||
"View channel on YouTube": "YouTube에서 채널 보기",
|
||||
"View playlist on YouTube": "유튜브에서 재생목록 보기",
|
||||
"View channel on YouTube": "유튜브에서 채널 보기",
|
||||
"Subscribe": "구독",
|
||||
"Unsubscribe": "구독 취소",
|
||||
"LIVE": "실시간",
|
||||
|
@ -116,11 +116,11 @@
|
|||
"Show replies": "댓글 보기",
|
||||
"Hide replies": "댓글 숨기기",
|
||||
"Incorrect password": "잘못된 비밀번호",
|
||||
"License: ": "라이선스: ",
|
||||
"License: ": "라이센스: ",
|
||||
"Genre: ": "장르: ",
|
||||
"Editing playlist `x`": "재생목록 `x` 수정하기",
|
||||
"Playlist privacy": "재생목록 공개 범위",
|
||||
"Watch on YouTube": "YouTube에서 보기",
|
||||
"Watch on YouTube": "유튜브에서 보기",
|
||||
"Show less": "간략히",
|
||||
"Show more": "더보기",
|
||||
"Title": "제목",
|
||||
|
@ -129,13 +129,13 @@
|
|||
"Delete playlist": "재생목록 삭제",
|
||||
"Delete playlist `x`?": "재생목록 `x` 를 삭제 하시겠습니까?",
|
||||
"Updated `x` ago": "`x` 전에 업데이트됨",
|
||||
"Released under the AGPLv3 on Github.": "GitHub에 AGPLv3 으로 배포됩니다.",
|
||||
"Released under the AGPLv3 on Github.": "깃허브에 AGPLv3 으로 배포됩니다.",
|
||||
"View all playlists": "모든 재생목록 보기",
|
||||
"Private": "비공개",
|
||||
"Unlisted": "목록에 없음",
|
||||
"Public": "공개",
|
||||
"View privacy policy.": "개인정보 처리방침 보기.",
|
||||
"View JavaScript license information.": "JavaScript 라이센스 정보 보기.",
|
||||
"View JavaScript license information.": "자바스크립트 라이센스 정보 보기.",
|
||||
"Source available here.": "소스는 여기에서 사용할 수 있습니다.",
|
||||
"Log out": "로그아웃",
|
||||
"search": "검색",
|
||||
|
@ -202,7 +202,7 @@
|
|||
"search_filters_features_option_hdr": "HDR",
|
||||
"Current version: ": "현재 버전: ",
|
||||
"next_steps_error_message_refresh": "새로 고침",
|
||||
"next_steps_error_message_go_to_youtube": "YouTube로 가기",
|
||||
"next_steps_error_message_go_to_youtube": "유튜브로 가기",
|
||||
"search_filters_features_option_subtitles": "자막",
|
||||
"`x` marked it with a ❤": "`x`님의 ❤",
|
||||
"Download as: ": "다음으로 다운로드: ",
|
||||
|
@ -245,14 +245,14 @@
|
|||
"Could not create mix.": "믹스를 생성할 수 없습니다.",
|
||||
"`x` ago": "`x` 전",
|
||||
"comments_view_x_replies_0": "답글 {{count}}개 보기",
|
||||
"View Reddit comments": "Reddit의 댓글 보기",
|
||||
"View Reddit comments": "레딧 댓글 보기",
|
||||
"Engagement: ": "약속: ",
|
||||
"Wilson score: ": "Wilson Score: ",
|
||||
"Family friendly? ": "가족 친화적입니까? ",
|
||||
"Family friendly? ": "전연령 영상입니까? ",
|
||||
"Quota exceeded, try again in a few hours": "한도량을 초과했습니다. 몇 시간 후에 다시 시도하세요",
|
||||
"View `x` comments": {
|
||||
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` 개의 댓글 보기",
|
||||
"": "`x` 개의 댓글 보기"
|
||||
"([^.,0-9]|^)1([^.,0-9]|$)": "`x`개의 댓글 보기",
|
||||
"": "`x`개의 댓글 보기"
|
||||
},
|
||||
"Haitian Creole": "아이티 크레올어",
|
||||
"Gujarati": "구자라트어",
|
||||
|
@ -273,16 +273,16 @@
|
|||
"Bosnian": "보스니아어",
|
||||
"Belarusian": "벨라루스어",
|
||||
"Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "로그인할 수 없습니다. 이중 인증(Authenticator 또는 SMS)이 켜져 있는지 확인하세요.",
|
||||
"View more comments on Reddit": "Reddit에서 더 많은 댓글 보기",
|
||||
"View YouTube comments": "YouTube 댓글 보기",
|
||||
"Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "JavaScript가 꺼져 있는 것 같습니다! 댓글을 보려면 여기를 클릭하세요. 댓글을 로드하는 데 시간이 조금 더 걸릴 수 있습니다.",
|
||||
"Shared `x`": "공유된 `x`",
|
||||
"View more comments on Reddit": "레딧에서 더 많은 댓글 보기",
|
||||
"View YouTube comments": "유튜브 댓글 보기",
|
||||
"Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "자바스크립트가 꺼져 있는 것 같습니다! 댓글을 보려면 여기를 클릭하세요. 댓글을 로드하는 데 시간이 조금 더 걸릴 수 있습니다.",
|
||||
"Shared `x`": "`x` 업로드",
|
||||
"Whitelisted regions: ": "차단되지 않은 지역: ",
|
||||
"search_filters_sort_option_views": "조회수",
|
||||
"Please log in": "로그인하세요",
|
||||
"Password cannot be longer than 55 characters": "비밀번호는 55자 이하여야 합니다",
|
||||
"Password cannot be empty": "비밀번호는 비워둘 수 없습니다",
|
||||
"Please sign in using 'Log in with Google'": "'Google로 로그인'을 사용하여 로그인하세요",
|
||||
"Please sign in using 'Log in with Google'": "'구글로 로그인'을 사용하여 로그인하세요",
|
||||
"Wrong username or password": "잘못된 사용자 이름 또는 비밀번호",
|
||||
"Password is a required field": "비밀번호는 필수 필드입니다",
|
||||
"User ID is a required field": "사용자 ID는 필수 필드입니다",
|
||||
|
@ -312,13 +312,13 @@
|
|||
"Fallback comments: ": "대체 댓글: ",
|
||||
"Swahili": "스와힐리어",
|
||||
"Sundanese": "순다어",
|
||||
"generic_count_years_0": "{{count}} 년",
|
||||
"generic_count_months_0": "{{count}} 개월",
|
||||
"generic_count_weeks_0": "{{count}} 주",
|
||||
"generic_count_days_0": "{{count}} 일",
|
||||
"generic_count_hours_0": "{{count}} 시간",
|
||||
"generic_count_minutes_0": "{{count}} 분",
|
||||
"generic_count_seconds_0": "{{count}} 초",
|
||||
"generic_count_years_0": "{{count}}년",
|
||||
"generic_count_months_0": "{{count}}개월",
|
||||
"generic_count_weeks_0": "{{count}}주",
|
||||
"generic_count_days_0": "{{count}}일",
|
||||
"generic_count_hours_0": "{{count}}시간",
|
||||
"generic_count_minutes_0": "{{count}}분",
|
||||
"generic_count_seconds_0": "{{count}}초",
|
||||
"Zulu": "줄루어",
|
||||
"Yoruba": "요루바어",
|
||||
"Yiddish": "이디시어",
|
||||
|
@ -337,9 +337,9 @@
|
|||
"Swedish": "스웨덴어",
|
||||
"Spanish (Latin America)": "스페인어 (라틴 아메리카)",
|
||||
"comments_points_count_0": "{{count}} 포인트",
|
||||
"Invidious Private Feed for `x`": "`x` 에 대한 Invidious 비공개 피드",
|
||||
"Invidious Private Feed for `x`": "`x` 에 대한 인비디어스 비공개 피드",
|
||||
"Premieres `x`": "최초 공개 `x`",
|
||||
"Premieres in `x`": "`x` 에 최초 공개",
|
||||
"Premieres in `x`": "`x` 후 최초 공개",
|
||||
"next_steps_error_message": "다음 방법을 시도해 보세요: ",
|
||||
"search_filters_features_option_c_commons": "크리에이티브 커먼즈",
|
||||
"search_filters_duration_label": "길이",
|
||||
|
@ -352,7 +352,7 @@
|
|||
"Video mode": "비디오 모드",
|
||||
"Audio mode": "오디오 모드",
|
||||
"permalink": "퍼머링크",
|
||||
"YouTube comment permalink": "YouTube 댓글 퍼머링크",
|
||||
"YouTube comment permalink": "유튜브 댓글 퍼머링크",
|
||||
"(edited)": "(수정됨)",
|
||||
"%A %B %-d, %Y": "%A %B %-d, %Y",
|
||||
"Movies": "영화",
|
||||
|
@ -396,7 +396,7 @@
|
|||
"search_filters_features_option_purchased": "구입한 항목",
|
||||
"search_filters_apply_button": "선택한 필터 적용하기",
|
||||
"preferences_quality_dash_option_240p": "240p",
|
||||
"preferences_region_label": "콘텐트 국가: ",
|
||||
"preferences_region_label": "국가: ",
|
||||
"preferences_quality_dash_option_1440p": "1440p",
|
||||
"French (auto-generated)": "프랑스어 (자동 생성됨)",
|
||||
"Indonesian (auto-generated)": "인도네시아어 (자동 생성됨)",
|
||||
|
@ -404,19 +404,19 @@
|
|||
"Vietnamese (auto-generated)": "베트남어 (자동 생성됨)",
|
||||
"preferences_quality_dash_option_2160p": "2160p",
|
||||
"Italian (auto-generated)": "이탈리아어 (자동 생성됨)",
|
||||
"preferences_quality_option_medium": "중간",
|
||||
"preferences_quality_option_medium": "보통",
|
||||
"preferences_quality_dash_option_720p": "720p",
|
||||
"search_filters_duration_option_medium": "중간 (4 - 20분)",
|
||||
"preferences_quality_dash_option_best": "최고",
|
||||
"Portuguese (auto-generated)": "포르투갈어 (자동 생성됨)",
|
||||
"Spanish (Spain)": "스페인어 (스페인)",
|
||||
"preferences_quality_dash_label": "선호하시는 DASH 비디오 품질: ",
|
||||
"preferences_quality_dash_label": "선호하는 DASH 비디오 품질: ",
|
||||
"preferences_quality_option_hd720": "HD720",
|
||||
"Spanish (auto-generated)": "스페인어 (자동 생성됨)",
|
||||
"preferences_quality_dash_option_1080p": "1080p",
|
||||
"preferences_quality_dash_option_worst": "최저",
|
||||
"preferences_watch_history_label": "시청 기록 활성화: ",
|
||||
"invidious": "Invidious",
|
||||
"invidious": "인비디어스",
|
||||
"preferences_quality_option_small": "낮음",
|
||||
"preferences_quality_dash_option_auto": "자동",
|
||||
"preferences_quality_dash_option_480p": "480p",
|
||||
|
@ -437,7 +437,24 @@
|
|||
"Spanish (Mexico)": "스페인어 (멕시코)",
|
||||
"search_filters_type_option_all": "모든 유형",
|
||||
"footer_donate_page": "기부하기",
|
||||
"preferences_quality_option_dash": "DASH (적절한 화질)",
|
||||
"preferences_quality_option_dash": "DASH (다양한 화질)",
|
||||
"preferences_quality_dash_option_360p": "360p",
|
||||
"preferences_save_player_pos_label": "이어서 보기 활성화 "
|
||||
"preferences_save_player_pos_label": "이어서 보기 활성화: ",
|
||||
"none": "없음",
|
||||
"videoinfo_started_streaming_x_ago": "`x` 전에 스트리밍을 시작했습니다",
|
||||
"crash_page_you_found_a_bug": "Invidious에서 버그를 찾은 것 같습니다!",
|
||||
"download_subtitles": "자막 - `x`(.vtt)",
|
||||
"user_saved_playlists": "`x`개의 저장된 재생목록",
|
||||
"crash_page_before_reporting": "버그를 보고하기 전에 다음 사항이 있는지 확인합니다:",
|
||||
"crash_page_search_issue": "<a href=\"`x`\">깃허브에서 기존 이슈</a>를 검색했습니다",
|
||||
"Video unavailable": "비디오를 사용할 수 없음",
|
||||
"crash_page_refresh": "<a href=\"`x`\">페이지를 새로고침</a>하려고 했습니다",
|
||||
"videoinfo_watch_on_youTube": "유튜브에서 보기",
|
||||
"crash_page_switch_instance": "<a href=\"`x`\">다른 인스턴스를 사용</a>하려고 했습니다",
|
||||
"crash_page_read_the_faq": "<a href=\"`x`\">자주 묻는 질문(FAQ)</a> 읽기",
|
||||
"user_created_playlists": "`x`개의 생성된 재생목록",
|
||||
"crash_page_report_issue": "위의 방법 중 어느 것도 도움이 되지 않았다면, <a href=\"`x`\">깃허브에서 새 이슈를 열고</a>(가능하면 영어로) 메시지에 다음 텍스트를 포함하세요(해당 텍스트를 번역하지 마십시오):",
|
||||
"videoinfo_youTube_embed_link": "임베드",
|
||||
"videoinfo_invidious_embed_link": "임베드 링크",
|
||||
"error_video_not_in_playlist": "요청한 동영상이 이 재생목록에 없습니다. <a href=\"`x`\">재생목록 목록을 보려면 여기를 클릭하십시오.</a>"
|
||||
}
|
||||
|
|
129
locales/lt.json
129
locales/lt.json
|
@ -21,15 +21,15 @@
|
|||
"No": "Ne",
|
||||
"Import and Export Data": "Importuoti ir eksportuoti duomenis",
|
||||
"Import": "Importuoti",
|
||||
"Import Invidious data": "Importuoti Invidious duomenis",
|
||||
"Import YouTube subscriptions": "Importuoti YouTube prenumeratas",
|
||||
"Import Invidious data": "Importuoti Invidious JSON duomenis",
|
||||
"Import YouTube subscriptions": "Importuoti YouTube/OPML prenumeratas",
|
||||
"Import FreeTube subscriptions (.db)": "Importuoti FreeTube prenumeratas (.db)",
|
||||
"Import NewPipe subscriptions (.json)": "Importuoti NewPipe prenumeratas (.json)",
|
||||
"Import NewPipe data (.zip)": "Importuoti NewPipe duomenis (.zip)",
|
||||
"Export": "Eksportuoti",
|
||||
"Export subscriptions as OPML": "Eksportuoti prenumeratas kaip OPML",
|
||||
"Export subscriptions as OPML (for NewPipe & FreeTube)": "Eksportuoti prenumeratas kaip OPML (skirta NewPipe & FreeTube)",
|
||||
"Export data as JSON": "Eksportuoti duomenis kaip JSON",
|
||||
"Export data as JSON": "Eksportuoti Invidious duomenis kaip JSON",
|
||||
"Delete account?": "Ištrinti paskyrą?",
|
||||
"History": "Istorija",
|
||||
"An alternative front-end to YouTube": "Alternatyvus YouTube žiūrėjimo būdas",
|
||||
|
@ -66,7 +66,7 @@
|
|||
"preferences_related_videos_label": "Rodyti susijusius vaizdo įrašus: ",
|
||||
"preferences_annotations_label": "Rodyti anotacijas pagal nutylėjimą: ",
|
||||
"preferences_extend_desc_label": "Automatiškai išplėsti vaizdo įrašo aprašymą: ",
|
||||
"preferences_vr_mode_label": "Interaktyvūs 360 laipsnių vaizdo įrašai: ",
|
||||
"preferences_vr_mode_label": "Interaktyvūs 360 laipsnių vaizdo įrašai (reikalingas WebGL): ",
|
||||
"preferences_category_visual": "Vizualinės nuostatos",
|
||||
"preferences_player_style_label": "Vaizdo grotuvo stilius: ",
|
||||
"Dark mode: ": "Tamsus rėžimas: ",
|
||||
|
@ -153,7 +153,7 @@
|
|||
"Shared `x`": "Pasidalino `x`",
|
||||
"Premieres in `x`": "Premjera už `x`",
|
||||
"Premieres `x`": "Premjera`x`",
|
||||
"Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Sveiki! Atrodo, kad turite išjungę \"JavaScript\". Spauskite čia norėdami peržiūrėti komentarus, turėkite omenyje, kad jų įkėlimas gali užtrukti.",
|
||||
"Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Sveiki! Panašu, kad turite išjungę „JavaScript“. Spustelėkite čia norėdami peržiūrėti komentarus, atminkite, kad jų įkėlimas gali užtrukti šiek tiek ilgiau.",
|
||||
"View YouTube comments": "Žiūrėti YouTube komentarus",
|
||||
"View more comments on Reddit": "Žiūrėti daugiau komentarų Reddit",
|
||||
"View `x` comments": {
|
||||
|
@ -371,5 +371,122 @@
|
|||
"preferences_quality_dash_option_best": "Geriausia",
|
||||
"preferences_quality_dash_option_worst": "Blogiausia",
|
||||
"preferences_quality_dash_option_auto": "Automatinis",
|
||||
"search_filters_title": "Filtras"
|
||||
"search_filters_title": "Filtras",
|
||||
"generic_videos_count_0": "{{count}} vaizdo įrašas",
|
||||
"generic_videos_count_1": "{{count}} vaizdo įrašai",
|
||||
"generic_videos_count_2": "{{count}} vaizdo įrašų",
|
||||
"generic_subscribers_count_0": "{{count}} prenumeratorius",
|
||||
"generic_subscribers_count_1": "{{count}} prenumeratoriai",
|
||||
"generic_subscribers_count_2": "{{count}} prenumeratorių",
|
||||
"generic_subscriptions_count_0": "{{count}} prenumerata",
|
||||
"generic_subscriptions_count_1": "{{count}} prenumeratos",
|
||||
"generic_subscriptions_count_2": "{{count}} prenumeratų",
|
||||
"preferences_watch_history_label": "Įgalinti žiūrėjimo istoriją: ",
|
||||
"preferences_quality_dash_option_1080p": "1080p",
|
||||
"invidious": "Invidious",
|
||||
"preferences_quality_dash_option_720p": "720p",
|
||||
"generic_playlists_count_0": "{{count}} grojaraštis",
|
||||
"generic_playlists_count_1": "{{count}} grojaraščiai",
|
||||
"generic_playlists_count_2": "{{count}} grojaraščių",
|
||||
"preferences_quality_option_medium": "Vidutinė",
|
||||
"preferences_quality_option_small": "Maža",
|
||||
"preferences_quality_dash_option_4320p": "4320p",
|
||||
"preferences_quality_dash_option_1440p": "1440p",
|
||||
"preferences_quality_dash_option_2160p": "2160p",
|
||||
"preferences_quality_dash_option_144p": "144p",
|
||||
"preferences_quality_option_hd720": "HD720",
|
||||
"preferences_quality_dash_option_360p": "360p",
|
||||
"preferences_quality_option_dash": "DASH (prisitaikanti kokybė)",
|
||||
"generic_views_count_0": "{{count}} peržiūra",
|
||||
"generic_views_count_1": "{{count}} peržiūros",
|
||||
"generic_views_count_2": "{{count}} peržiūrų",
|
||||
"preferences_quality_dash_option_480p": "480p",
|
||||
"preferences_quality_dash_option_240p": "240p",
|
||||
"none": "nėra",
|
||||
"search_filters_type_option_all": "Bet koks tipas",
|
||||
"videoinfo_started_streaming_x_ago": "Pradėjo transliuoti prieš `x`",
|
||||
"crash_page_switch_instance": "pabandėte <a href=\"`x`\">naudoti kitą perdavimo šaltinį</a>",
|
||||
"search_filters_duration_option_none": "Bet kokia trukmė",
|
||||
"search_filters_duration_option_medium": "Vidutinio ilgumo (4 - 20 minučių)",
|
||||
"search_filters_features_option_vr180": "VR180",
|
||||
"crash_page_before_reporting": "Prieš pranešdami apie klaidą įsitikinkite, kad:",
|
||||
"crash_page_read_the_faq": "perskaitėte <a href=\"`x`\">Dažniausiai užduodamus klausimus (DUK)</a>",
|
||||
"crash_page_search_issue": "ieškojote <a href=\"`x`\"> esamų problemų GitHub</a>",
|
||||
"error_video_not_in_playlist": "Prašomo vaizdo įrašo šiame grojaraštyje nėra. <a href=\"`x`\">Spustelėkite čia, kad pamatytumėte grojaraščio pagrindinį puslapį.</a>",
|
||||
"crash_page_report_issue": "Jei nė vienas iš pirmiau pateiktų būdų nepadėjo, prašome <a href=\"`x`\">atidaryti naują problemą GitHub</a> (pageidautina anglų kalba) ir į savo pranešimą įtraukti šį tekstą (NEVERSKITE šio teksto):",
|
||||
"subscriptions_unseen_notifs_count_0": "{{count}} nematytas pranešimas",
|
||||
"subscriptions_unseen_notifs_count_1": "{{count}} nematyti pranešimai",
|
||||
"subscriptions_unseen_notifs_count_2": "{{count}} nematytų pranešimų",
|
||||
"Vietnamese (auto-generated)": "Vietnamiečių kalba (automatiškai sugeneruota)",
|
||||
"Dutch (auto-generated)": "Olandų kalba (automatiškai sugeneruota)",
|
||||
"generic_count_weeks_0": "{{count}} savaitę",
|
||||
"generic_count_weeks_1": "{{count}} savaitės",
|
||||
"generic_count_weeks_2": "{{count}} savaičių",
|
||||
"Interlingue": "Interlingue",
|
||||
"Italian (auto-generated)": "Italų kalba (automatiškai sugeneruota)",
|
||||
"Japanese (auto-generated)": "Japonų kalba (automatiškai sugeneruota)",
|
||||
"Korean (auto-generated)": "Korėjiečių kalba (automatiškai sugeneruota)",
|
||||
"generic_count_months_0": "{{count}} mėnesį",
|
||||
"generic_count_months_1": "{{count}} mėnesius",
|
||||
"generic_count_months_2": "{{count}} mėnesių",
|
||||
"generic_count_days_0": "{{count}} dieną",
|
||||
"generic_count_days_1": "{{count}} dienas",
|
||||
"generic_count_days_2": "{{count}} dienų",
|
||||
"generic_count_hours_0": "{{count}} valandą",
|
||||
"generic_count_hours_1": "{{count}} valandas",
|
||||
"generic_count_hours_2": "{{count}} valandų",
|
||||
"generic_count_seconds_0": "{{count}} sekundę",
|
||||
"generic_count_seconds_1": "{{count}} sekundes",
|
||||
"generic_count_seconds_2": "{{count}} sekundžių",
|
||||
"generic_count_minutes_0": "{{count}} minutę",
|
||||
"generic_count_minutes_1": "{{count}} minutes",
|
||||
"generic_count_minutes_2": "{{count}} minučių",
|
||||
"generic_count_years_0": "{{count}} metus",
|
||||
"generic_count_years_1": "{{count}} metus",
|
||||
"generic_count_years_2": "{{count}} metų",
|
||||
"Popular enabled: ": "Populiarūs įgalinti: ",
|
||||
"Portuguese (auto-generated)": "Portugalų kalba (automatiškai sugeneruota)",
|
||||
"videoinfo_watch_on_youTube": "Žiaurėti Youtube",
|
||||
"Chinese (China)": "Kinų kalba (Kinija)",
|
||||
"crash_page_you_found_a_bug": "Atrodo, kad radote \"Invidious\" klaidą!",
|
||||
"search_filters_features_option_three_sixty": "360°",
|
||||
"English (United Kingdom)": "Anglų kalba (Jungtinė Karalystė)",
|
||||
"Chinese (Hong Kong)": "Kinų kalba (Honkongas)",
|
||||
"search_message_change_filters_or_query": "Pabandykite išplėsti paieškos užklausą ir (arba) pakeisti filtrus.",
|
||||
"English (United States)": "Anglų kalba (Jungtinės Amerikos Valstijos)",
|
||||
"Chinese (Taiwan)": "Kinų kalba (Taivanas)",
|
||||
"search_message_use_another_instance": " Taip pat galite <a href=\"`x`\">ieškoti kitame perdavimo šaltinyje</a>.",
|
||||
"tokens_count_0": "{{count}} žetonas",
|
||||
"tokens_count_1": "{{count}} žetonai",
|
||||
"tokens_count_2": "{{count}} žetonų",
|
||||
"search_message_no_results": "Rezultatų nerasta.",
|
||||
"comments_view_x_replies_0": "Žiūrėti {{count}} atsakymą",
|
||||
"comments_view_x_replies_1": "Žiūrėti {{count}} atsakymus",
|
||||
"comments_view_x_replies_2": "Žiūrėti {{count}} atsakymų",
|
||||
"comments_points_count_0": "{{count}} taškas",
|
||||
"comments_points_count_1": "{{count}} taškai",
|
||||
"comments_points_count_2": "{{count}} taškų",
|
||||
"Cantonese (Hong Kong)": "Kantono kalba (Honkongas)",
|
||||
"Chinese": "Kinų",
|
||||
"French (auto-generated)": "Prancūzų kalba (automatiškai sugeneruota)",
|
||||
"German (auto-generated)": "Vokiečių kalba (automatiškai sugeneruota)",
|
||||
"Indonesian (auto-generated)": "Indoneziečių kalba (automatiškai sugeneruota)",
|
||||
"Portuguese (Brazil)": "Portugalų kalba (Brazilija)",
|
||||
"Russian (auto-generated)": "Rusų kalba (automatiškai sugeneruota)",
|
||||
"Spanish (Mexico)": "Ispanų kalba (Meksika)",
|
||||
"Spanish (auto-generated)": "Ispanų kalba (automatiškai sugeneruota)",
|
||||
"Spanish (Spain)": "Ispanų kalba (Ispanija)",
|
||||
"Turkish (auto-generated)": "Turkų kalba (automatiškai sugeneruota)",
|
||||
"search_filters_date_label": "Įkėlimo data",
|
||||
"search_filters_date_option_none": "Bet kokia data",
|
||||
"search_filters_features_option_purchased": "Įsigyta",
|
||||
"search_filters_apply_button": "Taikyti pasirinktus filtrus",
|
||||
"download_subtitles": "Subtitrai - `x` (.vtt)",
|
||||
"user_created_playlists": "`x` sukurti grojaraščiai",
|
||||
"user_saved_playlists": "`x` išsaugoti grojaraščiai",
|
||||
"Video unavailable": "Vaizdo įrašas nepasiekiamas",
|
||||
"preferences_save_player_pos_label": "Išsaugoti atkūrimo padėtį: ",
|
||||
"videoinfo_youTube_embed_link": "Įterpti",
|
||||
"videoinfo_invidious_embed_link": "Įterpti nuorodą",
|
||||
"crash_page_refresh": "pabandėte <a href=\"`x`\">atnaujinti puslapį</a>"
|
||||
}
|
||||
|
|
|
@ -471,5 +471,6 @@
|
|||
"search_filters_date_label": "Opplastningsdato",
|
||||
"search_filters_apply_button": "Bruk valgte filtre",
|
||||
"search_filters_date_option_none": "Siden begynnelsen",
|
||||
"search_filters_features_option_vr180": "VR180"
|
||||
"search_filters_features_option_vr180": "VR180",
|
||||
"error_video_not_in_playlist": "Forespurt video finnes ikke i denne spillelisten. <a href=\"`x`\">Trykk her for spillelistens hjemmeside.</a>"
|
||||
}
|
||||
|
|
|
@ -177,7 +177,7 @@
|
|||
"Not a playlist.": "Không phải danh sách phát.",
|
||||
"Playlist does not exist.": "Danh sách phát không tồn tại.",
|
||||
"Could not pull trending pages.": "Không thể kéo các trang thịnh hành.",
|
||||
"Hidden field \"challenge\" is a required field": "Trường ẩn \"challenge\" là trường bắt buộc",
|
||||
"Hidden field \"challenge\" is a required field": "Trường ẩn \"challenge\" là trường bắt buộc",
|
||||
"Hidden field \"token\" is a required field": "Trường ẩn \"token\" là trường bắt buộc",
|
||||
"Erroneous challenge": "Thử thách sai",
|
||||
"Erroneous token": "Mã thông báo bị lỗi",
|
||||
|
@ -341,5 +341,10 @@
|
|||
"search_filters_features_option_location": "vị trí",
|
||||
"search_filters_features_option_hdr": "hdr",
|
||||
"Current version: ": "Phiên bản hiện tại: ",
|
||||
"search_filters_title": "bộ lọc"
|
||||
"search_filters_title": "bộ lọc",
|
||||
"generic_playlists_count": "{{count}} danh sách phát",
|
||||
"generic_views_count": "{{count}} lượt xem",
|
||||
"View `x` comments": {
|
||||
"": "Xem `x` bình luận"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,13 +5,13 @@ CONFIG = Config.from_yaml(File.open("config/config.example.yml"))
|
|||
Spectator.describe "Helper" do
|
||||
describe "#produce_channel_videos_url" do
|
||||
it "correctly produces url for requesting page `x` of a channel's videos" do
|
||||
expect(produce_channel_videos_url(ucid: "UCXuqSBlHAE6Xw-yeJA0Tunw")).to eq("/browse_ajax?continuation=4qmFsgI8EhhVQ1h1cVNCbEhBRTZYdy15ZUpBMFR1bncaIEVnWjJhV1JsYjNNd0FqZ0JZQUZxQUxnQkFDQUFlZ0V4&gl=US&hl=en")
|
||||
# expect(produce_channel_videos_url(ucid: "UCXuqSBlHAE6Xw-yeJA0Tunw")).to eq("/browse_ajax?continuation=4qmFsgI8EhhVQ1h1cVNCbEhBRTZYdy15ZUpBMFR1bncaIEVnWjJhV1JsYjNNd0FqZ0JZQUZxQUxnQkFDQUFlZ0V4&gl=US&hl=en")
|
||||
#
|
||||
# expect(produce_channel_videos_url(ucid: "UCXuqSBlHAE6Xw-yeJA0Tunw", sort_by: "popular")).to eq("/browse_ajax?continuation=4qmFsgJAEhhVQ1h1cVNCbEhBRTZYdy15ZUpBMFR1bncaJEVnWjJhV1JsYjNNd0FqZ0JZQUZxQUxnQkFDQUFlZ0V4R0FFPQ%3D%3D&gl=US&hl=en")
|
||||
|
||||
expect(produce_channel_videos_url(ucid: "UCXuqSBlHAE6Xw-yeJA0Tunw", sort_by: "popular")).to eq("/browse_ajax?continuation=4qmFsgJAEhhVQ1h1cVNCbEhBRTZYdy15ZUpBMFR1bncaJEVnWjJhV1JsYjNNd0FqZ0JZQUZxQUxnQkFDQUFlZ0V4R0FFPQ%3D%3D&gl=US&hl=en")
|
||||
# expect(produce_channel_videos_url(ucid: "UCXuqSBlHAE6Xw-yeJA0Tunw", page: 20)).to eq("/browse_ajax?continuation=4qmFsgJAEhhVQ1h1cVNCbEhBRTZYdy15ZUpBMFR1bncaJEVnWjJhV1JsYjNNd0FqZ0JZQUZxQUxnQkFDQUFlZ0l5TUE9PQ%3D%3D&gl=US&hl=en")
|
||||
|
||||
expect(produce_channel_videos_url(ucid: "UCXuqSBlHAE6Xw-yeJA0Tunw", page: 20)).to eq("/browse_ajax?continuation=4qmFsgJAEhhVQ1h1cVNCbEhBRTZYdy15ZUpBMFR1bncaJEVnWjJhV1JsYjNNd0FqZ0JZQUZxQUxnQkFDQUFlZ0l5TUE9PQ%3D%3D&gl=US&hl=en")
|
||||
|
||||
expect(produce_channel_videos_url(ucid: "UC-9-kyTW8ZkZNDHQJ6FgpwQ", page: 20, sort_by: "popular")).to eq("/browse_ajax?continuation=4qmFsgJAEhhVQy05LWt5VFc4WmtaTkRIUUo2Rmdwd1EaJEVnWjJhV1JsYjNNd0FqZ0JZQUZxQUxnQkFDQUFlZ0l5TUJnQg%3D%3D&gl=US&hl=en")
|
||||
# expect(produce_channel_videos_url(ucid: "UC-9-kyTW8ZkZNDHQJ6FgpwQ", page: 20, sort_by: "popular")).to eq("/browse_ajax?continuation=4qmFsgJAEhhVQy05LWt5VFc4WmtaTkRIUUo2Rmdwd1EaJEVnWjJhV1JsYjNNd0FqZ0JZQUZxQUxnQkFDQUFlZ0l5TUJnQg%3D%3D&gl=US&hl=en")
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -172,6 +172,8 @@ end
|
|||
CONNECTION_CHANNEL = Channel({Bool, Channel(PQ::Notification)}).new(32)
|
||||
Invidious::Jobs.register Invidious::Jobs::NotificationJob.new(CONNECTION_CHANNEL, CONFIG.database_url)
|
||||
|
||||
Invidious::Jobs.register Invidious::Jobs::ClearExpiredItemsJob.new
|
||||
|
||||
Invidious::Jobs.start_all
|
||||
|
||||
def popular_videos
|
||||
|
|
|
@ -1,53 +1,48 @@
|
|||
def produce_channel_videos_continuation(ucid, page = 1, auto_generated = nil, sort_by = "newest", v2 = false)
|
||||
object = {
|
||||
"80226972:embedded" => {
|
||||
"2:string" => ucid,
|
||||
"3:base64" => {
|
||||
"2:string" => "videos",
|
||||
"6:varint" => 2_i64,
|
||||
"7:varint" => 1_i64,
|
||||
"12:varint" => 1_i64,
|
||||
"13:string" => "",
|
||||
"23:varint" => 0_i64,
|
||||
object_inner_2 = {
|
||||
"2:0:embedded" => {
|
||||
"1:0:varint" => 0_i64,
|
||||
},
|
||||
"5:varint" => 50_i64,
|
||||
"6:varint" => 1_i64,
|
||||
"7:varint" => (page * 30).to_i64,
|
||||
"9:varint" => 1_i64,
|
||||
"10:varint" => 0_i64,
|
||||
}
|
||||
|
||||
object_inner_2_encoded = object_inner_2
|
||||
.try { |i| Protodec::Any.cast_json(i) }
|
||||
.try { |i| Protodec::Any.from_json(i) }
|
||||
.try { |i| Base64.urlsafe_encode(i) }
|
||||
.try { |i| URI.encode_www_form(i) }
|
||||
|
||||
object_inner_1 = {
|
||||
"110:embedded" => {
|
||||
"3:embedded" => {
|
||||
"15:embedded" => {
|
||||
"1:embedded" => {
|
||||
"1:string" => object_inner_2_encoded,
|
||||
"2:string" => "00000000-0000-0000-0000-000000000000",
|
||||
},
|
||||
"3:varint" => 1_i64,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if !v2
|
||||
if auto_generated
|
||||
seed = Time.unix(1525757349)
|
||||
until seed >= Time.utc
|
||||
seed += 1.month
|
||||
end
|
||||
timestamp = seed - (page - 1).months
|
||||
object_inner_1_encoded = object_inner_1
|
||||
.try { |i| Protodec::Any.cast_json(i) }
|
||||
.try { |i| Protodec::Any.from_json(i) }
|
||||
.try { |i| Base64.urlsafe_encode(i) }
|
||||
.try { |i| URI.encode_www_form(i) }
|
||||
|
||||
object["80226972:embedded"]["3:base64"].as(Hash)["4:varint"] = 0x36_i64
|
||||
object["80226972:embedded"]["3:base64"].as(Hash)["15:string"] = "#{timestamp.to_unix}"
|
||||
else
|
||||
object["80226972:embedded"]["3:base64"].as(Hash)["4:varint"] = 0_i64
|
||||
object["80226972:embedded"]["3:base64"].as(Hash)["15:string"] = "#{page}"
|
||||
end
|
||||
else
|
||||
object["80226972:embedded"]["3:base64"].as(Hash)["4:varint"] = 0_i64
|
||||
|
||||
object["80226972:embedded"]["3:base64"].as(Hash)["61:string"] = Base64.urlsafe_encode(Protodec::Any.from_json(Protodec::Any.cast_json({
|
||||
"1:string" => Base64.urlsafe_encode(Protodec::Any.from_json(Protodec::Any.cast_json({
|
||||
"1:varint" => 30_i64 * (page - 1),
|
||||
}))),
|
||||
})))
|
||||
end
|
||||
|
||||
case sort_by
|
||||
when "newest"
|
||||
when "popular"
|
||||
object["80226972:embedded"]["3:base64"].as(Hash)["3:varint"] = 0x01_i64
|
||||
when "oldest"
|
||||
object["80226972:embedded"]["3:base64"].as(Hash)["3:varint"] = 0x02_i64
|
||||
else nil # Ignore
|
||||
end
|
||||
|
||||
object["80226972:embedded"]["3:string"] = Base64.urlsafe_encode(Protodec::Any.from_json(Protodec::Any.cast_json(object["80226972:embedded"]["3:base64"])))
|
||||
object["80226972:embedded"].delete("3:base64")
|
||||
object = {
|
||||
"80226972:embedded" => {
|
||||
"2:string" => ucid,
|
||||
"3:string" => object_inner_1_encoded,
|
||||
"35:string" => "browse-feed#{ucid}videos102",
|
||||
},
|
||||
}
|
||||
|
||||
continuation = object.try { |i| Protodec::Any.cast_json(i) }
|
||||
.try { |i| Protodec::Any.from_json(i) }
|
||||
|
@ -67,10 +62,11 @@ end
|
|||
def get_60_videos(ucid, author, page, auto_generated, sort_by = "newest")
|
||||
videos = [] of SearchVideo
|
||||
|
||||
2.times do |i|
|
||||
initial_data = get_channel_videos_response(ucid, page * 2 + (i - 1), auto_generated: auto_generated, sort_by: sort_by)
|
||||
videos.concat extract_videos(initial_data, author, ucid)
|
||||
end
|
||||
# 2.times do |i|
|
||||
# initial_data = get_channel_videos_response(ucid, page * 2 + (i - 1), auto_generated: auto_generated, sort_by: sort_by)
|
||||
initial_data = get_channel_videos_response(ucid, 1, auto_generated: auto_generated, sort_by: sort_by)
|
||||
videos = extract_videos(initial_data, author, ucid)
|
||||
# end
|
||||
|
||||
return videos.size, videos
|
||||
end
|
||||
|
|
|
@ -78,6 +78,10 @@ class Config
|
|||
property decrypt_polling : Bool = false
|
||||
# Used for crawling channels: threads should check all videos uploaded by a channel
|
||||
property full_refresh : Bool = false
|
||||
|
||||
# Jobs config structure. See jobs.cr and jobs/base_job.cr
|
||||
property jobs = Invidious::Jobs::JobsConfig.new
|
||||
|
||||
# Used to tell Invidious it is behind a proxy, so links to resources should be https://
|
||||
property https_only : Bool?
|
||||
# HMAC signing key for CSRF tokens and verifying pubsub subscriptions
|
||||
|
|
|
@ -4,7 +4,7 @@ module Invidious::Database::Nonces
|
|||
extend self
|
||||
|
||||
# -------------------
|
||||
# Insert
|
||||
# Insert / Delete
|
||||
# -------------------
|
||||
|
||||
def insert(nonce : String, expire : Time)
|
||||
|
@ -17,6 +17,15 @@ module Invidious::Database::Nonces
|
|||
PG_DB.exec(request, nonce, expire)
|
||||
end
|
||||
|
||||
def delete_expired
|
||||
request = <<-SQL
|
||||
DELETE FROM nonces *
|
||||
WHERE expire < now()
|
||||
SQL
|
||||
|
||||
PG_DB.exec(request)
|
||||
end
|
||||
|
||||
# -------------------
|
||||
# Update
|
||||
# -------------------
|
||||
|
|
|
@ -22,6 +22,15 @@ module Invidious::Database::Videos
|
|||
PG_DB.exec(request, id)
|
||||
end
|
||||
|
||||
def delete_expired
|
||||
request = <<-SQL
|
||||
DELETE FROM videos *
|
||||
WHERE updated < (now() - interval '6 hours')
|
||||
SQL
|
||||
|
||||
PG_DB.exec(request)
|
||||
end
|
||||
|
||||
def update(video : Video)
|
||||
request = <<-SQL
|
||||
UPDATE videos
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
# "bn_BD" => load_locale("bn_BD"), # Bengali (Bangladesh) [Incomplete]
|
||||
# "eu" => load_locale("eu"), # Basque [Incomplete]
|
||||
# "sk" => load_locale("sk"), # Slovak [Incomplete]
|
||||
LOCALES_LIST = {
|
||||
"ar" => "العربية", # Arabic
|
||||
"bn" => "বাংলা", # Bengali
|
||||
"ca" => "Català", # Catalan
|
||||
"cs" => "Čeština", # Czech
|
||||
"da" => "Dansk", # Danish
|
||||
"de" => "Deutsch", # German
|
||||
|
@ -11,6 +10,7 @@ LOCALES_LIST = {
|
|||
"eo" => "Esperanto", # Esperanto
|
||||
"es" => "Español", # Spanish
|
||||
"et" => "Eesti keel", # Estonian
|
||||
"eu" => "Euskara", # Basque
|
||||
"fa" => "فارسی", # Persian
|
||||
"fi" => "Suomi", # Finnish
|
||||
"fr" => "Français", # French
|
||||
|
@ -32,6 +32,8 @@ LOCALES_LIST = {
|
|||
"pt-PT" => "Português de Portugal", # Portuguese (Portugal)
|
||||
"ro" => "Română", # Romanian
|
||||
"ru" => "Русский", # Russian
|
||||
"si" => "සිංහල", # Sinhala
|
||||
"sk" => "Slovenčina", # Slovak
|
||||
"sl" => "Slovenščina", # Slovenian
|
||||
"sq" => "Shqip", # Albanian
|
||||
"sr" => "Srpski (latinica)", # Serbian (Latin)
|
||||
|
|
|
@ -1,12 +1,39 @@
|
|||
module Invidious::Jobs
|
||||
JOBS = [] of BaseJob
|
||||
|
||||
# Automatically generate a structure that wraps the various
|
||||
# jobs' configs, so that the follwing YAML config can be used:
|
||||
#
|
||||
# jobs:
|
||||
# job_name:
|
||||
# enabled: true
|
||||
# some_property: "value"
|
||||
#
|
||||
macro finished
|
||||
struct JobsConfig
|
||||
include YAML::Serializable
|
||||
|
||||
{% for sc in BaseJob.subclasses %}
|
||||
# Voodoo macro to transform `Some::Module::CustomJob` to `custom`
|
||||
{% class_name = sc.id.split("::").last.id.gsub(/Job$/, "").underscore %}
|
||||
|
||||
getter {{ class_name }} = {{ sc.name }}::Config.new
|
||||
{% end %}
|
||||
|
||||
def initialize
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.register(job : BaseJob)
|
||||
JOBS << job
|
||||
end
|
||||
|
||||
def self.start_all
|
||||
JOBS.each do |job|
|
||||
# Don't run the main rountine if the job is disabled by config
|
||||
next if job.disabled?
|
||||
|
||||
spawn { job.begin }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,3 +1,33 @@
|
|||
abstract class Invidious::Jobs::BaseJob
|
||||
abstract def begin
|
||||
|
||||
# When this base job class is inherited, make sure to define
|
||||
# a basic "Config" structure, that contains the "enable" property,
|
||||
# and to create the associated instance property.
|
||||
#
|
||||
macro inherited
|
||||
macro finished
|
||||
# This config structure can be expanded as required.
|
||||
struct Config
|
||||
include YAML::Serializable
|
||||
|
||||
property enable = true
|
||||
|
||||
def initialize
|
||||
end
|
||||
end
|
||||
|
||||
property cfg = Config.new
|
||||
|
||||
# Return true if job is enabled by config
|
||||
protected def enabled? : Bool
|
||||
return (@cfg.enable == true)
|
||||
end
|
||||
|
||||
# Return true if job is disabled by config
|
||||
protected def disabled? : Bool
|
||||
return (@cfg.enable == false)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
class Invidious::Jobs::ClearExpiredItemsJob < Invidious::Jobs::BaseJob
|
||||
# Remove items (videos, nonces, etc..) whose cache is outdated every hour.
|
||||
# Removes the need for a cron job.
|
||||
def begin
|
||||
loop do
|
||||
failed = false
|
||||
|
||||
LOGGER.info("jobs: running ClearExpiredItems job")
|
||||
|
||||
begin
|
||||
Invidious::Database::Videos.delete_expired
|
||||
Invidious::Database::Nonces.delete_expired
|
||||
rescue DB::Error
|
||||
failed = true
|
||||
end
|
||||
|
||||
# Retry earlier than scheduled on DB error
|
||||
if failed
|
||||
LOGGER.info("jobs: ClearExpiredItems failed. Retrying in 10 minutes.")
|
||||
sleep 10.minutes
|
||||
else
|
||||
LOGGER.info("jobs: ClearExpiredItems done.")
|
||||
sleep 1.hour
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,7 +1,20 @@
|
|||
<% ucid = channel.ucid %>
|
||||
<% author = HTML.escape(channel.author) %>
|
||||
<% channel_profile_pic = URI.parse(channel.author_thumbnail).request_target %>
|
||||
|
||||
<% content_for "header" do %>
|
||||
<meta name="description" content="<%= channel.description %>">
|
||||
<meta property="og:site_name" content="Invidious">
|
||||
<meta property="og:url" content="<%= HOST_URL %>/channel/<%= ucid %>">
|
||||
<meta property="og:title" content="<%= author %>">
|
||||
<meta property="og:image" content="/ggpht<%= channel_profile_pic %>">
|
||||
<meta property="og:description" content="<%= channel.description %>">
|
||||
<meta name="twitter:card" content="summary">
|
||||
<meta name="twitter:url" content="<%= HOST_URL %>/channel/<%= ucid %>">
|
||||
<meta name="twitter:title" content="<%= author %>">
|
||||
<meta name="twitter:description" content="<%= channel.description %>">
|
||||
<meta name="twitter:image" content="/ggpht<%= channel_profile_pic %>">
|
||||
<link rel="alternate" href="https://www.youtube.com/channel/<%= ucid %>">
|
||||
<title><%= author %> - Invidious</title>
|
||||
<link rel="alternate" type="application/rss+xml" title="RSS" href="/feed/channel/<%= ucid %>" />
|
||||
<% end %>
|
||||
|
@ -19,7 +32,7 @@
|
|||
<div class="pure-g h-box">
|
||||
<div class="pure-u-2-3">
|
||||
<div class="channel-profile">
|
||||
<img src="/ggpht<%= URI.parse(channel.author_thumbnail).request_target %>">
|
||||
<img src="/ggpht<%= channel_profile_pic %>">
|
||||
<span><%= author %></span><% if !channel.verified.nil? && channel.verified %> <i class="icon ion ion-md-checkmark-circle"></i><% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<meta name="thumbnail" content="<%= thumbnail %>">
|
||||
<meta name="description" content="<%= HTML.escape(video.short_description) %>">
|
||||
<meta name="keywords" content="<%= video.keywords.join(",") %>">
|
||||
<meta property="og:site_name" content="Invidious">
|
||||
<meta property="og:site_name" content="<%= author %> | Invidious">
|
||||
<meta property="og:url" content="<%= HOST_URL %>/watch?v=<%= video.id %>">
|
||||
<meta property="og:title" content="<%= title %>">
|
||||
<meta property="og:image" content="/vi/<%= video.id %>/maxres.jpg">
|
||||
|
@ -19,7 +19,6 @@
|
|||
<meta property="og:video:width" content="1280">
|
||||
<meta property="og:video:height" content="720">
|
||||
<meta name="twitter:card" content="player">
|
||||
<meta name="twitter:site" content="@omarroth1">
|
||||
<meta name="twitter:url" content="<%= HOST_URL %>/watch?v=<%= video.id %>">
|
||||
<meta name="twitter:title" content="<%= title %>">
|
||||
<meta name="twitter:description" content="<%= HTML.escape(video.short_description) %>">
|
||||
|
|
|
@ -17,6 +17,7 @@ private ITEM_PARSERS = {
|
|||
Parsers::PlaylistRendererParser,
|
||||
Parsers::CategoryRendererParser,
|
||||
Parsers::RichItemRendererParser,
|
||||
Parsers::ReelItemRendererParser,
|
||||
}
|
||||
|
||||
record AuthorFallback, name : String, id : String
|
||||
|
@ -369,7 +370,7 @@ private module Parsers
|
|||
end
|
||||
|
||||
# Parses an InnerTube richItemRenderer into a SearchVideo.
|
||||
# Returns nil when the given object isn't a shelfRenderer
|
||||
# Returns nil when the given object isn't a RichItemRenderer
|
||||
#
|
||||
# A richItemRenderer seems to be a simple wrapper for a videoRenderer, used
|
||||
# by the result page for hashtags. It is located inside a continuationItems
|
||||
|
@ -390,6 +391,90 @@ private module Parsers
|
|||
return {{@type.name}}
|
||||
end
|
||||
end
|
||||
|
||||
# Parses an InnerTube reelItemRenderer into a SearchVideo.
|
||||
# Returns nil when the given object isn't a reelItemRenderer
|
||||
#
|
||||
# reelItemRenderer items are used in the new (2022) channel layout,
|
||||
# in the "shorts" tab.
|
||||
#
|
||||
module ReelItemRendererParser
|
||||
def self.process(item : JSON::Any, author_fallback : AuthorFallback)
|
||||
if item_contents = item["reelItemRenderer"]?
|
||||
return self.parse(item_contents, author_fallback)
|
||||
end
|
||||
end
|
||||
|
||||
private def self.parse(item_contents, author_fallback)
|
||||
video_id = item_contents["videoId"].as_s
|
||||
|
||||
video_details_container = item_contents.dig(
|
||||
"navigationEndpoint", "reelWatchEndpoint",
|
||||
"overlay", "reelPlayerOverlayRenderer",
|
||||
"reelPlayerHeaderSupportedRenderers",
|
||||
"reelPlayerHeaderRenderer"
|
||||
)
|
||||
|
||||
# Author infos
|
||||
|
||||
author = video_details_container
|
||||
.dig?("channelTitleText", "runs", 0, "text")
|
||||
.try &.as_s || author_fallback.name
|
||||
|
||||
ucid = video_details_container
|
||||
.dig?("channelNavigationEndpoint", "browseEndpoint", "browseId")
|
||||
.try &.as_s || author_fallback.id
|
||||
|
||||
# Title & publication date
|
||||
|
||||
title = video_details_container.dig?("reelTitleText")
|
||||
.try { |t| extract_text(t) } || ""
|
||||
|
||||
published = video_details_container
|
||||
.dig?("timestampText", "simpleText")
|
||||
.try { |t| decode_date(t.as_s) } || Time.utc
|
||||
|
||||
# View count
|
||||
|
||||
view_count_text = video_details_container.dig?("viewCountText", "simpleText")
|
||||
view_count_text ||= video_details_container
|
||||
.dig?("viewCountText", "accessibility", "accessibilityData", "label")
|
||||
|
||||
view_count = view_count_text.try &.as_s.gsub(/\D+/, "").to_i64? || 0_i64
|
||||
|
||||
# Duration
|
||||
|
||||
a11y_data = item_contents
|
||||
.dig?("accessibility", "accessibilityData", "label")
|
||||
.try &.as_s || ""
|
||||
|
||||
regex_match = /- (?<min>\d+ minutes? )?(?<sec>\d+ seconds?)+ -/.match(a11y_data)
|
||||
|
||||
minutes = regex_match.try &.["min"].to_i(strict: false) || 0
|
||||
seconds = regex_match.try &.["sec"].to_i(strict: false) || 0
|
||||
|
||||
duration = (minutes*60 + seconds)
|
||||
|
||||
SearchVideo.new({
|
||||
title: title,
|
||||
id: video_id,
|
||||
author: author,
|
||||
ucid: ucid,
|
||||
published: published,
|
||||
views: view_count,
|
||||
description_html: "",
|
||||
length_seconds: duration,
|
||||
live_now: false,
|
||||
premium: false,
|
||||
premiere_timestamp: Time.unix(0),
|
||||
author_verified: false,
|
||||
})
|
||||
end
|
||||
|
||||
def self.parser_name
|
||||
return {{@type.name}}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# The following are the extractors for extracting an array of items from
|
||||
|
@ -436,21 +521,31 @@ private module Extractors
|
|||
content = extract_selected_tab(target["tabs"])["content"]
|
||||
|
||||
if section_list_contents = content.dig?("sectionListRenderer", "contents")
|
||||
section_list_contents.as_a.each do |renderer_container|
|
||||
renderer_container_contents = renderer_container["itemSectionRenderer"]["contents"][0]
|
||||
raw_items = unpack_section_list(section_list_contents)
|
||||
elsif rich_grid_contents = content.dig?("richGridRenderer", "contents")
|
||||
raw_items = rich_grid_contents.as_a
|
||||
end
|
||||
|
||||
# Category extraction
|
||||
if items_container = renderer_container_contents["shelfRenderer"]?
|
||||
raw_items << renderer_container_contents
|
||||
next
|
||||
elsif items_container = renderer_container_contents["gridRenderer"]?
|
||||
else
|
||||
items_container = renderer_container_contents
|
||||
end
|
||||
return raw_items
|
||||
end
|
||||
|
||||
items_container["items"]?.try &.as_a.each do |item|
|
||||
raw_items << item
|
||||
end
|
||||
private def self.unpack_section_list(contents)
|
||||
raw_items = [] of JSON::Any
|
||||
|
||||
contents.as_a.each do |renderer_container|
|
||||
renderer_container_contents = renderer_container["itemSectionRenderer"]["contents"][0]
|
||||
|
||||
# Category extraction
|
||||
if items_container = renderer_container_contents["shelfRenderer"]?
|
||||
raw_items << renderer_container_contents
|
||||
next
|
||||
elsif items_container = renderer_container_contents["gridRenderer"]?
|
||||
else
|
||||
items_container = renderer_container_contents
|
||||
end
|
||||
|
||||
items_container["items"]?.try &.as_a.each do |item|
|
||||
raw_items << item
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -525,14 +620,11 @@ private module Extractors
|
|||
end
|
||||
|
||||
private def self.extract(target)
|
||||
raw_items = [] of JSON::Any
|
||||
if content = target["gridContinuation"]?
|
||||
raw_items = content["items"].as_a
|
||||
elsif content = target["continuationItems"]?
|
||||
raw_items = content.as_a
|
||||
end
|
||||
content = target["continuationItems"]?
|
||||
content ||= target.dig?("gridContinuation", "items")
|
||||
content ||= target.dig?("richGridContinuation", "contents")
|
||||
|
||||
return raw_items
|
||||
return content.nil? ? [] of JSON::Any : content.as_a
|
||||
end
|
||||
|
||||
def self.extractor_name
|
||||
|
|
読み込み中…
新しいイシューから参照