Merge branch 'master' of github.com:iv-org/invidious
このコミットが含まれているのは:
コミット
5bbd2be78d
|
@ -38,10 +38,10 @@ jobs:
|
||||||
matrix:
|
matrix:
|
||||||
stable: [true]
|
stable: [true]
|
||||||
crystal:
|
crystal:
|
||||||
- 1.2.2
|
|
||||||
- 1.3.2
|
- 1.3.2
|
||||||
- 1.4.0
|
- 1.4.1
|
||||||
- 1.5.0
|
- 1.5.1
|
||||||
|
- 1.6.1
|
||||||
include:
|
include:
|
||||||
- crystal: nightly
|
- crystal: nightly
|
||||||
stable: false
|
stable: false
|
||||||
|
@ -52,7 +52,7 @@ jobs:
|
||||||
submodules: true
|
submodules: true
|
||||||
|
|
||||||
- name: Install Crystal
|
- name: Install Crystal
|
||||||
uses: crystal-lang/install-crystal@v1.6.0
|
uses: crystal-lang/install-crystal@v1.7.0
|
||||||
with:
|
with:
|
||||||
crystal: ${{ matrix.crystal }}
|
crystal: ${{ matrix.crystal }}
|
||||||
|
|
||||||
|
|
|
@ -52,7 +52,7 @@ jobs:
|
||||||
username: ${{ secrets.QUAY_USERNAME }}
|
username: ${{ secrets.QUAY_USERNAME }}
|
||||||
password: ${{ secrets.QUAY_PASSWORD }}
|
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'
|
if: github.ref == 'refs/heads/master'
|
||||||
uses: docker/build-push-action@v3
|
uses: docker/build-push-action@v3
|
||||||
with:
|
with:
|
||||||
|
@ -62,9 +62,11 @@ jobs:
|
||||||
labels: quay.expires-after=12w
|
labels: quay.expires-after=12w
|
||||||
push: true
|
push: true
|
||||||
tags: quay.io/invidious/invidious:${{ github.sha }},quay.io/invidious/invidious:latest
|
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'
|
if: github.ref == 'refs/heads/master'
|
||||||
uses: docker/build-push-action@v3
|
uses: docker/build-push-action@v3
|
||||||
with:
|
with:
|
||||||
|
@ -74,4 +76,30 @@ jobs:
|
||||||
labels: quay.expires-after=12w
|
labels: quay.expires-after=12w
|
||||||
push: true
|
push: true
|
||||||
tags: quay.io/invidious/invidious:${{ github.sha }}-arm64,quay.io/invidious/invidious:latest-arm64
|
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
|
build-args: release=1
|
||||||
|
|
2
Makefile
2
Makefile
|
@ -5,7 +5,7 @@
|
||||||
RELEASE := 1
|
RELEASE := 1
|
||||||
STATIC := 0
|
STATIC := 0
|
||||||
|
|
||||||
DISABLE_QUIC := 0
|
DISABLE_QUIC := 1
|
||||||
NO_DBG_SYMBOLS := 0
|
NO_DBG_SYMBOLS := 0
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -304,10 +304,8 @@ https_only: false
|
||||||
## Number of threads to use when crawling channel videos (during
|
## Number of threads to use when crawling channel videos (during
|
||||||
## subscriptions update).
|
## subscriptions update).
|
||||||
##
|
##
|
||||||
## Notes:
|
## Notes: This setting is overridden if either "-c THREADS" or
|
||||||
## - Setting this to 0 will disable the channel videos crawl job.
|
## "--channel-threads=THREADS" is passed on the command line.
|
||||||
## - This setting is overridden if "-c THREADS" or
|
|
||||||
## "--channel-threads=THREADS" are passed on the command line.
|
|
||||||
##
|
##
|
||||||
## Accepted values: a positive integer
|
## Accepted values: a positive integer
|
||||||
## Default: 1
|
## Default: 1
|
||||||
|
@ -335,10 +333,8 @@ full_refresh: false
|
||||||
##
|
##
|
||||||
## Number of threads to use when updating RSS feeds.
|
## Number of threads to use when updating RSS feeds.
|
||||||
##
|
##
|
||||||
## Notes:
|
## Notes: This setting is overridden if either "-f THREADS" or
|
||||||
## - Setting this to 0 will disable the channel videos crawl job.
|
## "--feed-threads=THREADS" is passed on the command line.
|
||||||
## - This setting is overridden if "-f THREADS" or
|
|
||||||
## "--feed-threads=THREADS" are passed on the command line.
|
|
||||||
##
|
##
|
||||||
## Accepted values: a positive integer
|
## Accepted values: a positive integer
|
||||||
## Default: 1
|
## Default: 1
|
||||||
|
@ -361,6 +357,39 @@ feed_threads: 1
|
||||||
#decrypt_polling: false
|
#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
|
# Captcha API
|
||||||
# -----------------------------
|
# -----------------------------
|
||||||
|
|
|
@ -2,6 +2,7 @@ FROM crystallang/crystal:1.4.1-alpine AS builder
|
||||||
RUN apk add --no-cache sqlite-static yaml-static
|
RUN apk add --no-cache sqlite-static yaml-static
|
||||||
|
|
||||||
ARG release
|
ARG release
|
||||||
|
ARG disable_quic
|
||||||
|
|
||||||
WORKDIR /invidious
|
WORKDIR /invidious
|
||||||
COPY ./shard.yml ./shard.yml
|
COPY ./shard.yml ./shard.yml
|
||||||
|
@ -23,7 +24,13 @@ COPY ./videojs-dependencies.yml ./videojs-dependencies.yml
|
||||||
RUN crystal spec --warnings all \
|
RUN crystal spec --warnings all \
|
||||||
--link-flags "-lxml2 -llzma"
|
--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 \
|
crystal build ./src/invidious.cr \
|
||||||
--release \
|
--release \
|
||||||
--static --warnings all \
|
--static --warnings all \
|
||||||
|
@ -35,7 +42,7 @@ RUN if [ "${release}" == 1 ] ; then \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
||||||
FROM alpine:latest
|
FROM alpine:3.16
|
||||||
RUN apk add --no-cache librsvg ttf-opensans
|
RUN apk add --no-cache librsvg ttf-opensans
|
||||||
WORKDIR /invidious
|
WORKDIR /invidious
|
||||||
RUN addgroup -g 1000 -S 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
|
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 release
|
||||||
|
ARG disable_quic
|
||||||
|
|
||||||
WORKDIR /invidious
|
WORKDIR /invidious
|
||||||
COPY ./shard.yml ./shard.yml
|
COPY ./shard.yml ./shard.yml
|
||||||
|
@ -23,7 +24,13 @@ COPY ./videojs-dependencies.yml ./videojs-dependencies.yml
|
||||||
RUN crystal spec --warnings all \
|
RUN crystal spec --warnings all \
|
||||||
--link-flags "-lxml2 -llzma"
|
--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 \
|
crystal build ./src/invidious.cr \
|
||||||
--release \
|
--release \
|
||||||
--static --warnings all \
|
--static --warnings all \
|
||||||
|
|
|
@ -34,8 +34,6 @@ securityContext:
|
||||||
|
|
||||||
# See https://github.com/bitnami/charts/tree/master/bitnami/postgresql
|
# See https://github.com/bitnami/charts/tree/master/bitnami/postgresql
|
||||||
postgresql:
|
postgresql:
|
||||||
image:
|
|
||||||
registry: quay.io
|
|
||||||
auth:
|
auth:
|
||||||
username: kemal
|
username: kemal
|
||||||
password: kemal
|
password: kemal
|
||||||
|
|
118
locales/eo.json
118
locales/eo.json
|
@ -21,15 +21,15 @@
|
||||||
"No": "Ne",
|
"No": "Ne",
|
||||||
"Import and Export Data": "Importi kaj Eksporti Datumojn",
|
"Import and Export Data": "Importi kaj Eksporti Datumojn",
|
||||||
"Import": "Importi",
|
"Import": "Importi",
|
||||||
"Import Invidious data": "Importi datumojn de Invidious",
|
"Import Invidious data": "Importi JSON-datumojn de Invidious",
|
||||||
"Import YouTube subscriptions": "Importi abonojn de JuTubo",
|
"Import YouTube subscriptions": "Importi abonojn de YouTube/OPML",
|
||||||
"Import FreeTube subscriptions (.db)": "Importi abonojn de FreeTube (.db)",
|
"Import FreeTube subscriptions (.db)": "Importi abonojn de FreeTube (.db)",
|
||||||
"Import NewPipe subscriptions (.json)": "Importi abonojn de NewPipe (.json)",
|
"Import NewPipe subscriptions (.json)": "Importi abonojn de NewPipe (.json)",
|
||||||
"Import NewPipe data (.zip)": "Importi datumojn de NewPipe (.zip)",
|
"Import NewPipe data (.zip)": "Importi datumojn de NewPipe (.zip)",
|
||||||
"Export": "Eksporti",
|
"Export": "Eksporti",
|
||||||
"Export subscriptions as OPML": "Eksporti abonojn kiel OPML",
|
"Export subscriptions as OPML": "Eksporti abonojn kiel OPML",
|
||||||
"Export subscriptions as OPML (for NewPipe & FreeTube)": "Eksporti abonojn kiel OPML (por NewPipe kaj FreeTube)",
|
"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?",
|
"Delete account?": "Ĉu forigi konton?",
|
||||||
"History": "Historio",
|
"History": "Historio",
|
||||||
"An alternative front-end to YouTube": "Alternativa fasado al JuTubo",
|
"An alternative front-end to YouTube": "Alternativa fasado al JuTubo",
|
||||||
|
@ -66,7 +66,7 @@
|
||||||
"preferences_related_videos_label": "Ĉu montri rilatajn filmetojn? ",
|
"preferences_related_videos_label": "Ĉu montri rilatajn filmetojn? ",
|
||||||
"preferences_annotations_label": "Ĉu montri prinotojn defaŭlte? ",
|
"preferences_annotations_label": "Ĉu montri prinotojn defaŭlte? ",
|
||||||
"preferences_extend_desc_label": "Aŭtomate etendi priskribon de filmeto: ",
|
"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_category_visual": "Vidaj preferoj",
|
||||||
"preferences_player_style_label": "Ludila stilo: ",
|
"preferences_player_style_label": "Ludila stilo: ",
|
||||||
"Dark mode: ": "Malhela reĝimo: ",
|
"Dark mode: ": "Malhela reĝimo: ",
|
||||||
|
@ -75,7 +75,7 @@
|
||||||
"light": "hela",
|
"light": "hela",
|
||||||
"preferences_thin_mode_label": "Maldika reĝimo: ",
|
"preferences_thin_mode_label": "Maldika reĝimo: ",
|
||||||
"preferences_category_misc": "Aliaj agordoj",
|
"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_category_subscription": "Abonaj agordoj",
|
||||||
"preferences_annotations_subscribed_label": "Ĉu montri prinotojn defaŭlte por abonitaj kanaloj? ",
|
"preferences_annotations_subscribed_label": "Ĉu montri prinotojn defaŭlte por abonitaj kanaloj? ",
|
||||||
"Redirect homepage to feed: ": "Alidirekti hejmpâgon al fluo: ",
|
"Redirect homepage to feed: ": "Alidirekti hejmpâgon al fluo: ",
|
||||||
|
@ -140,7 +140,7 @@
|
||||||
"Show more": "Montri pli",
|
"Show more": "Montri pli",
|
||||||
"Show less": "Montri malpli",
|
"Show less": "Montri malpli",
|
||||||
"Watch on YouTube": "Vidi filmeton en JuTubo",
|
"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",
|
"Hide annotations": "Kaŝi prinotojn",
|
||||||
"Show annotations": "Montri prinotojn",
|
"Show annotations": "Montri prinotojn",
|
||||||
"Genre: ": "Ĝenro: ",
|
"Genre: ": "Ĝenro: ",
|
||||||
|
@ -368,5 +368,109 @@
|
||||||
"footer_donate_page": "Donaci",
|
"footer_donate_page": "Donaci",
|
||||||
"preferences_region_label": "Lando de la enhavo: ",
|
"preferences_region_label": "Lando de la enhavo: ",
|
||||||
"preferences_quality_dash_label": "Preferata DASH-a videkvalito: ",
|
"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",
|
"Save preferences": "Guardar las preferencias",
|
||||||
"Subscription manager": "Gestor de suscripciones",
|
"Subscription manager": "Gestor de suscripciones",
|
||||||
"Token manager": "Gestor de tokens",
|
"Token manager": "Gestor de tokens",
|
||||||
"Token": "Token",
|
"Token": "Ficha",
|
||||||
"Import/export": "Importar/Exportar",
|
"Import/export": "Importar/Exportar",
|
||||||
"unsubscribe": "Desuscribirse",
|
"unsubscribe": "Desuscribirse",
|
||||||
"revoke": "revocar",
|
"revoke": "revocar",
|
||||||
|
@ -355,7 +355,7 @@
|
||||||
"search_filters_features_option_location": "ubicación",
|
"search_filters_features_option_location": "ubicación",
|
||||||
"search_filters_features_option_hdr": "hdr",
|
"search_filters_features_option_hdr": "hdr",
|
||||||
"Current version: ": "Versión actual: ",
|
"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_refresh": "Recargar la página",
|
||||||
"next_steps_error_message_go_to_youtube": "Ir a YouTube",
|
"next_steps_error_message_go_to_youtube": "Ir a YouTube",
|
||||||
"search_filters_duration_option_short": "Corto (< 4 minutos)",
|
"search_filters_duration_option_short": "Corto (< 4 minutos)",
|
||||||
|
@ -467,8 +467,8 @@
|
||||||
"search_filters_duration_option_none": "Cualquier duración",
|
"search_filters_duration_option_none": "Cualquier duración",
|
||||||
"search_filters_features_option_vr180": "VR180",
|
"search_filters_features_option_vr180": "VR180",
|
||||||
"search_filters_apply_button": "Aplicar filtros seleccionados",
|
"search_filters_apply_button": "Aplicar filtros seleccionados",
|
||||||
"tokens_count": "{{count}} token",
|
"tokens_count": "{{count}} ficha",
|
||||||
"tokens_count_plural": "{{count}} tokens",
|
"tokens_count_plural": "{{count}} fichas",
|
||||||
"search_message_use_another_instance": " También puede <a href=\"`x`\">buscar en otra instancia</a>.",
|
"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)",
|
"search_filters_duration_option_medium": "Medio (4 - 20 minutes)",
|
||||||
"Popular enabled: ": "¿Habilitar la sección popular? ",
|
"Popular enabled: ": "¿Habilitar la sección popular? ",
|
||||||
|
|
|
@ -12,22 +12,22 @@
|
||||||
"Dark mode: ": "다크 모드: ",
|
"Dark mode: ": "다크 모드: ",
|
||||||
"preferences_player_style_label": "플레이어 스타일: ",
|
"preferences_player_style_label": "플레이어 스타일: ",
|
||||||
"preferences_category_visual": "시각 설정",
|
"preferences_category_visual": "시각 설정",
|
||||||
"preferences_vr_mode_label": "360도 비디오와 상호작용하기(WebGL를 요구함): ",
|
"preferences_vr_mode_label": "VR 영상 활성화(WebGL 필요): ",
|
||||||
"preferences_extend_desc_label": "자동으로 비디오 설명을 확장: ",
|
"preferences_extend_desc_label": "자동으로 비디오 설명을 확장: ",
|
||||||
"preferences_annotations_label": "기본적으로 주석 표시: ",
|
"preferences_annotations_label": "기본으로 주석 표시: ",
|
||||||
"preferences_related_videos_label": "관련 동영상 보기: ",
|
"preferences_related_videos_label": "관련 동영상 보기: ",
|
||||||
"Fallback captions: ": "대체 자막: ",
|
"Fallback captions: ": "대체 자막: ",
|
||||||
"preferences_captions_label": "기본 자막: ",
|
"preferences_captions_label": "기본 자막: ",
|
||||||
"reddit": "Reddit",
|
"reddit": "레딧",
|
||||||
"youtube": "YouTube",
|
"youtube": "유튜브",
|
||||||
"preferences_comments_label": "기본 댓글: ",
|
"preferences_comments_label": "기본 댓글: ",
|
||||||
"preferences_volume_label": "플레이어 볼륨: ",
|
"preferences_volume_label": "플레이어 볼륨: ",
|
||||||
"preferences_quality_label": "선호하는 비디오 품질: ",
|
"preferences_quality_label": "선호하는 비디오 품질: ",
|
||||||
"preferences_speed_label": "기본 속도: ",
|
"preferences_speed_label": "기본 속도: ",
|
||||||
"preferences_local_label": "비디오를 프록시: ",
|
"preferences_local_label": "비디오를 프록시: ",
|
||||||
"preferences_listen_label": "기본적으로 듣기: ",
|
"preferences_listen_label": "라디오 모드 활성화: ",
|
||||||
"preferences_continue_autoplay_label": "다음 동영상 자동재생 ",
|
"preferences_continue_autoplay_label": "다음 동영상 자동재생 ",
|
||||||
"preferences_continue_label": "기본적으로 다음 재생: ",
|
"preferences_continue_label": "다음 동영상으로 이동: ",
|
||||||
"preferences_autoplay_label": "자동재생: ",
|
"preferences_autoplay_label": "자동재생: ",
|
||||||
"preferences_video_loop_label": "항상 반복: ",
|
"preferences_video_loop_label": "항상 반복: ",
|
||||||
"preferences_category_player": "플레이어 설정",
|
"preferences_category_player": "플레이어 설정",
|
||||||
|
@ -46,8 +46,8 @@
|
||||||
"Log in/register": "로그인/회원가입",
|
"Log in/register": "로그인/회원가입",
|
||||||
"Log in": "로그인",
|
"Log in": "로그인",
|
||||||
"source": "출처",
|
"source": "출처",
|
||||||
"JavaScript license information": "JavaScript 라이선스 정보",
|
"JavaScript license information": "자바스크립트 라이센스 정보",
|
||||||
"An alternative front-end to YouTube": "YouTube의 대안 프론트엔드",
|
"An alternative front-end to YouTube": "유튜브의 프론트엔드 대안",
|
||||||
"History": "역사",
|
"History": "역사",
|
||||||
"Delete account?": "계정을 삭제 하시겠습니까?",
|
"Delete account?": "계정을 삭제 하시겠습니까?",
|
||||||
"Export data as JSON": "데이터를 JSON으로 내보내기",
|
"Export data as JSON": "데이터를 JSON으로 내보내기",
|
||||||
|
@ -57,15 +57,15 @@
|
||||||
"Import NewPipe data (.zip)": "NewPipe 데이터 가져오기 (.zip)",
|
"Import NewPipe data (.zip)": "NewPipe 데이터 가져오기 (.zip)",
|
||||||
"Import NewPipe subscriptions (.json)": "NewPipe 구독을 가져오기 (.json)",
|
"Import NewPipe subscriptions (.json)": "NewPipe 구독을 가져오기 (.json)",
|
||||||
"Import FreeTube subscriptions (.db)": "FreeTube 구독 가져오기 (.db)",
|
"Import FreeTube subscriptions (.db)": "FreeTube 구독 가져오기 (.db)",
|
||||||
"Import YouTube subscriptions": "YouTube 구독 가져오기",
|
"Import YouTube subscriptions": "유튜브 구독 가져오기",
|
||||||
"Import Invidious data": "Invidious JSON 데이터 가져오기",
|
"Import Invidious data": "인비디어스 JSON 데이터 가져오기",
|
||||||
"Import": "가져오기",
|
"Import": "가져오기",
|
||||||
"Import and Export Data": "데이터 가져오기 및 내보내기",
|
"Import and Export Data": "데이터 가져오기 및 내보내기",
|
||||||
"No": "아니요",
|
"No": "아니요",
|
||||||
"Yes": "예",
|
"Yes": "예",
|
||||||
"Authorize token for `x`?": "`x` 에 대한 토큰을 승인하시겠습니까?",
|
"Authorize token for `x`?": "`x` 에 대한 토큰을 승인하시겠습니까?",
|
||||||
"Authorize token?": "토큰을 승인하시겠습니까?",
|
"Authorize token?": "토큰을 승인하시겠습니까?",
|
||||||
"Cannot change password for Google accounts": "Google 계정의 비밀번호를 변경할 수 없습니다",
|
"Cannot change password for Google accounts": "구글 계정의 비밀번호를 변경할 수 없습니다",
|
||||||
"New passwords must match": "새 비밀번호는 일치해야 합니다",
|
"New passwords must match": "새 비밀번호는 일치해야 합니다",
|
||||||
"New password": "새 비밀번호",
|
"New password": "새 비밀번호",
|
||||||
"Clear watch history?": "재생 기록을 삭제 하시겠습니까?",
|
"Clear watch history?": "재생 기록을 삭제 하시겠습니까?",
|
||||||
|
@ -76,8 +76,8 @@
|
||||||
"popular": "인기",
|
"popular": "인기",
|
||||||
"oldest": "오래된순",
|
"oldest": "오래된순",
|
||||||
"newest": "최신순",
|
"newest": "최신순",
|
||||||
"View playlist on YouTube": "YouTube에서 재생목록 보기",
|
"View playlist on YouTube": "유튜브에서 재생목록 보기",
|
||||||
"View channel on YouTube": "YouTube에서 채널 보기",
|
"View channel on YouTube": "유튜브에서 채널 보기",
|
||||||
"Subscribe": "구독",
|
"Subscribe": "구독",
|
||||||
"Unsubscribe": "구독 취소",
|
"Unsubscribe": "구독 취소",
|
||||||
"LIVE": "실시간",
|
"LIVE": "실시간",
|
||||||
|
@ -116,11 +116,11 @@
|
||||||
"Show replies": "댓글 보기",
|
"Show replies": "댓글 보기",
|
||||||
"Hide replies": "댓글 숨기기",
|
"Hide replies": "댓글 숨기기",
|
||||||
"Incorrect password": "잘못된 비밀번호",
|
"Incorrect password": "잘못된 비밀번호",
|
||||||
"License: ": "라이선스: ",
|
"License: ": "라이센스: ",
|
||||||
"Genre: ": "장르: ",
|
"Genre: ": "장르: ",
|
||||||
"Editing playlist `x`": "재생목록 `x` 수정하기",
|
"Editing playlist `x`": "재생목록 `x` 수정하기",
|
||||||
"Playlist privacy": "재생목록 공개 범위",
|
"Playlist privacy": "재생목록 공개 범위",
|
||||||
"Watch on YouTube": "YouTube에서 보기",
|
"Watch on YouTube": "유튜브에서 보기",
|
||||||
"Show less": "간략히",
|
"Show less": "간략히",
|
||||||
"Show more": "더보기",
|
"Show more": "더보기",
|
||||||
"Title": "제목",
|
"Title": "제목",
|
||||||
|
@ -129,13 +129,13 @@
|
||||||
"Delete playlist": "재생목록 삭제",
|
"Delete playlist": "재생목록 삭제",
|
||||||
"Delete playlist `x`?": "재생목록 `x` 를 삭제 하시겠습니까?",
|
"Delete playlist `x`?": "재생목록 `x` 를 삭제 하시겠습니까?",
|
||||||
"Updated `x` ago": "`x` 전에 업데이트됨",
|
"Updated `x` ago": "`x` 전에 업데이트됨",
|
||||||
"Released under the AGPLv3 on Github.": "GitHub에 AGPLv3 으로 배포됩니다.",
|
"Released under the AGPLv3 on Github.": "깃허브에 AGPLv3 으로 배포됩니다.",
|
||||||
"View all playlists": "모든 재생목록 보기",
|
"View all playlists": "모든 재생목록 보기",
|
||||||
"Private": "비공개",
|
"Private": "비공개",
|
||||||
"Unlisted": "목록에 없음",
|
"Unlisted": "목록에 없음",
|
||||||
"Public": "공개",
|
"Public": "공개",
|
||||||
"View privacy policy.": "개인정보 처리방침 보기.",
|
"View privacy policy.": "개인정보 처리방침 보기.",
|
||||||
"View JavaScript license information.": "JavaScript 라이센스 정보 보기.",
|
"View JavaScript license information.": "자바스크립트 라이센스 정보 보기.",
|
||||||
"Source available here.": "소스는 여기에서 사용할 수 있습니다.",
|
"Source available here.": "소스는 여기에서 사용할 수 있습니다.",
|
||||||
"Log out": "로그아웃",
|
"Log out": "로그아웃",
|
||||||
"search": "검색",
|
"search": "검색",
|
||||||
|
@ -202,7 +202,7 @@
|
||||||
"search_filters_features_option_hdr": "HDR",
|
"search_filters_features_option_hdr": "HDR",
|
||||||
"Current version: ": "현재 버전: ",
|
"Current version: ": "현재 버전: ",
|
||||||
"next_steps_error_message_refresh": "새로 고침",
|
"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": "자막",
|
"search_filters_features_option_subtitles": "자막",
|
||||||
"`x` marked it with a ❤": "`x`님의 ❤",
|
"`x` marked it with a ❤": "`x`님의 ❤",
|
||||||
"Download as: ": "다음으로 다운로드: ",
|
"Download as: ": "다음으로 다운로드: ",
|
||||||
|
@ -245,10 +245,10 @@
|
||||||
"Could not create mix.": "믹스를 생성할 수 없습니다.",
|
"Could not create mix.": "믹스를 생성할 수 없습니다.",
|
||||||
"`x` ago": "`x` 전",
|
"`x` ago": "`x` 전",
|
||||||
"comments_view_x_replies_0": "답글 {{count}}개 보기",
|
"comments_view_x_replies_0": "답글 {{count}}개 보기",
|
||||||
"View Reddit comments": "Reddit의 댓글 보기",
|
"View Reddit comments": "레딧 댓글 보기",
|
||||||
"Engagement: ": "약속: ",
|
"Engagement: ": "약속: ",
|
||||||
"Wilson score: ": "Wilson Score: ",
|
"Wilson score: ": "Wilson Score: ",
|
||||||
"Family friendly? ": "가족 친화적입니까? ",
|
"Family friendly? ": "전연령 영상입니까? ",
|
||||||
"Quota exceeded, try again in a few hours": "한도량을 초과했습니다. 몇 시간 후에 다시 시도하세요",
|
"Quota exceeded, try again in a few hours": "한도량을 초과했습니다. 몇 시간 후에 다시 시도하세요",
|
||||||
"View `x` comments": {
|
"View `x` comments": {
|
||||||
"([^.,0-9]|^)1([^.,0-9]|$)": "`x`개의 댓글 보기",
|
"([^.,0-9]|^)1([^.,0-9]|$)": "`x`개의 댓글 보기",
|
||||||
|
@ -273,16 +273,16 @@
|
||||||
"Bosnian": "보스니아어",
|
"Bosnian": "보스니아어",
|
||||||
"Belarusian": "벨라루스어",
|
"Belarusian": "벨라루스어",
|
||||||
"Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "로그인할 수 없습니다. 이중 인증(Authenticator 또는 SMS)이 켜져 있는지 확인하세요.",
|
"Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "로그인할 수 없습니다. 이중 인증(Authenticator 또는 SMS)이 켜져 있는지 확인하세요.",
|
||||||
"View more comments on Reddit": "Reddit에서 더 많은 댓글 보기",
|
"View more comments on Reddit": "레딧에서 더 많은 댓글 보기",
|
||||||
"View YouTube comments": "YouTube 댓글 보기",
|
"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.": "JavaScript가 꺼져 있는 것 같습니다! 댓글을 보려면 여기를 클릭하세요. 댓글을 로드하는 데 시간이 조금 더 걸릴 수 있습니다.",
|
"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`",
|
"Shared `x`": "`x` 업로드",
|
||||||
"Whitelisted regions: ": "차단되지 않은 지역: ",
|
"Whitelisted regions: ": "차단되지 않은 지역: ",
|
||||||
"search_filters_sort_option_views": "조회수",
|
"search_filters_sort_option_views": "조회수",
|
||||||
"Please log in": "로그인하세요",
|
"Please log in": "로그인하세요",
|
||||||
"Password cannot be longer than 55 characters": "비밀번호는 55자 이하여야 합니다",
|
"Password cannot be longer than 55 characters": "비밀번호는 55자 이하여야 합니다",
|
||||||
"Password cannot be empty": "비밀번호는 비워둘 수 없습니다",
|
"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": "잘못된 사용자 이름 또는 비밀번호",
|
"Wrong username or password": "잘못된 사용자 이름 또는 비밀번호",
|
||||||
"Password is a required field": "비밀번호는 필수 필드입니다",
|
"Password is a required field": "비밀번호는 필수 필드입니다",
|
||||||
"User ID is a required field": "사용자 ID는 필수 필드입니다",
|
"User ID is a required field": "사용자 ID는 필수 필드입니다",
|
||||||
|
@ -337,9 +337,9 @@
|
||||||
"Swedish": "스웨덴어",
|
"Swedish": "스웨덴어",
|
||||||
"Spanish (Latin America)": "스페인어 (라틴 아메리카)",
|
"Spanish (Latin America)": "스페인어 (라틴 아메리카)",
|
||||||
"comments_points_count_0": "{{count}} 포인트",
|
"comments_points_count_0": "{{count}} 포인트",
|
||||||
"Invidious Private Feed for `x`": "`x` 에 대한 Invidious 비공개 피드",
|
"Invidious Private Feed for `x`": "`x` 에 대한 인비디어스 비공개 피드",
|
||||||
"Premieres `x`": "최초 공개 `x`",
|
"Premieres `x`": "최초 공개 `x`",
|
||||||
"Premieres in `x`": "`x` 에 최초 공개",
|
"Premieres in `x`": "`x` 후 최초 공개",
|
||||||
"next_steps_error_message": "다음 방법을 시도해 보세요: ",
|
"next_steps_error_message": "다음 방법을 시도해 보세요: ",
|
||||||
"search_filters_features_option_c_commons": "크리에이티브 커먼즈",
|
"search_filters_features_option_c_commons": "크리에이티브 커먼즈",
|
||||||
"search_filters_duration_label": "길이",
|
"search_filters_duration_label": "길이",
|
||||||
|
@ -352,7 +352,7 @@
|
||||||
"Video mode": "비디오 모드",
|
"Video mode": "비디오 모드",
|
||||||
"Audio mode": "오디오 모드",
|
"Audio mode": "오디오 모드",
|
||||||
"permalink": "퍼머링크",
|
"permalink": "퍼머링크",
|
||||||
"YouTube comment permalink": "YouTube 댓글 퍼머링크",
|
"YouTube comment permalink": "유튜브 댓글 퍼머링크",
|
||||||
"(edited)": "(수정됨)",
|
"(edited)": "(수정됨)",
|
||||||
"%A %B %-d, %Y": "%A %B %-d, %Y",
|
"%A %B %-d, %Y": "%A %B %-d, %Y",
|
||||||
"Movies": "영화",
|
"Movies": "영화",
|
||||||
|
@ -396,7 +396,7 @@
|
||||||
"search_filters_features_option_purchased": "구입한 항목",
|
"search_filters_features_option_purchased": "구입한 항목",
|
||||||
"search_filters_apply_button": "선택한 필터 적용하기",
|
"search_filters_apply_button": "선택한 필터 적용하기",
|
||||||
"preferences_quality_dash_option_240p": "240p",
|
"preferences_quality_dash_option_240p": "240p",
|
||||||
"preferences_region_label": "콘텐트 국가: ",
|
"preferences_region_label": "국가: ",
|
||||||
"preferences_quality_dash_option_1440p": "1440p",
|
"preferences_quality_dash_option_1440p": "1440p",
|
||||||
"French (auto-generated)": "프랑스어 (자동 생성됨)",
|
"French (auto-generated)": "프랑스어 (자동 생성됨)",
|
||||||
"Indonesian (auto-generated)": "인도네시아어 (자동 생성됨)",
|
"Indonesian (auto-generated)": "인도네시아어 (자동 생성됨)",
|
||||||
|
@ -404,19 +404,19 @@
|
||||||
"Vietnamese (auto-generated)": "베트남어 (자동 생성됨)",
|
"Vietnamese (auto-generated)": "베트남어 (자동 생성됨)",
|
||||||
"preferences_quality_dash_option_2160p": "2160p",
|
"preferences_quality_dash_option_2160p": "2160p",
|
||||||
"Italian (auto-generated)": "이탈리아어 (자동 생성됨)",
|
"Italian (auto-generated)": "이탈리아어 (자동 생성됨)",
|
||||||
"preferences_quality_option_medium": "중간",
|
"preferences_quality_option_medium": "보통",
|
||||||
"preferences_quality_dash_option_720p": "720p",
|
"preferences_quality_dash_option_720p": "720p",
|
||||||
"search_filters_duration_option_medium": "중간 (4 - 20분)",
|
"search_filters_duration_option_medium": "중간 (4 - 20분)",
|
||||||
"preferences_quality_dash_option_best": "최고",
|
"preferences_quality_dash_option_best": "최고",
|
||||||
"Portuguese (auto-generated)": "포르투갈어 (자동 생성됨)",
|
"Portuguese (auto-generated)": "포르투갈어 (자동 생성됨)",
|
||||||
"Spanish (Spain)": "스페인어 (스페인)",
|
"Spanish (Spain)": "스페인어 (스페인)",
|
||||||
"preferences_quality_dash_label": "선호하시는 DASH 비디오 품질: ",
|
"preferences_quality_dash_label": "선호하는 DASH 비디오 품질: ",
|
||||||
"preferences_quality_option_hd720": "HD720",
|
"preferences_quality_option_hd720": "HD720",
|
||||||
"Spanish (auto-generated)": "스페인어 (자동 생성됨)",
|
"Spanish (auto-generated)": "스페인어 (자동 생성됨)",
|
||||||
"preferences_quality_dash_option_1080p": "1080p",
|
"preferences_quality_dash_option_1080p": "1080p",
|
||||||
"preferences_quality_dash_option_worst": "최저",
|
"preferences_quality_dash_option_worst": "최저",
|
||||||
"preferences_watch_history_label": "시청 기록 활성화: ",
|
"preferences_watch_history_label": "시청 기록 활성화: ",
|
||||||
"invidious": "Invidious",
|
"invidious": "인비디어스",
|
||||||
"preferences_quality_option_small": "낮음",
|
"preferences_quality_option_small": "낮음",
|
||||||
"preferences_quality_dash_option_auto": "자동",
|
"preferences_quality_dash_option_auto": "자동",
|
||||||
"preferences_quality_dash_option_480p": "480p",
|
"preferences_quality_dash_option_480p": "480p",
|
||||||
|
@ -437,7 +437,24 @@
|
||||||
"Spanish (Mexico)": "스페인어 (멕시코)",
|
"Spanish (Mexico)": "스페인어 (멕시코)",
|
||||||
"search_filters_type_option_all": "모든 유형",
|
"search_filters_type_option_all": "모든 유형",
|
||||||
"footer_donate_page": "기부하기",
|
"footer_donate_page": "기부하기",
|
||||||
"preferences_quality_option_dash": "DASH (적절한 화질)",
|
"preferences_quality_option_dash": "DASH (다양한 화질)",
|
||||||
"preferences_quality_dash_option_360p": "360p",
|
"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",
|
"No": "Ne",
|
||||||
"Import and Export Data": "Importuoti ir eksportuoti duomenis",
|
"Import and Export Data": "Importuoti ir eksportuoti duomenis",
|
||||||
"Import": "Importuoti",
|
"Import": "Importuoti",
|
||||||
"Import Invidious data": "Importuoti Invidious duomenis",
|
"Import Invidious data": "Importuoti Invidious JSON duomenis",
|
||||||
"Import YouTube subscriptions": "Importuoti YouTube prenumeratas",
|
"Import YouTube subscriptions": "Importuoti YouTube/OPML prenumeratas",
|
||||||
"Import FreeTube subscriptions (.db)": "Importuoti FreeTube prenumeratas (.db)",
|
"Import FreeTube subscriptions (.db)": "Importuoti FreeTube prenumeratas (.db)",
|
||||||
"Import NewPipe subscriptions (.json)": "Importuoti NewPipe prenumeratas (.json)",
|
"Import NewPipe subscriptions (.json)": "Importuoti NewPipe prenumeratas (.json)",
|
||||||
"Import NewPipe data (.zip)": "Importuoti NewPipe duomenis (.zip)",
|
"Import NewPipe data (.zip)": "Importuoti NewPipe duomenis (.zip)",
|
||||||
"Export": "Eksportuoti",
|
"Export": "Eksportuoti",
|
||||||
"Export subscriptions as OPML": "Eksportuoti prenumeratas kaip OPML",
|
"Export subscriptions as OPML": "Eksportuoti prenumeratas kaip OPML",
|
||||||
"Export subscriptions as OPML (for NewPipe & FreeTube)": "Eksportuoti prenumeratas kaip OPML (skirta NewPipe & FreeTube)",
|
"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ą?",
|
"Delete account?": "Ištrinti paskyrą?",
|
||||||
"History": "Istorija",
|
"History": "Istorija",
|
||||||
"An alternative front-end to YouTube": "Alternatyvus YouTube žiūrėjimo būdas",
|
"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_related_videos_label": "Rodyti susijusius vaizdo įrašus: ",
|
||||||
"preferences_annotations_label": "Rodyti anotacijas pagal nutylėjimą: ",
|
"preferences_annotations_label": "Rodyti anotacijas pagal nutylėjimą: ",
|
||||||
"preferences_extend_desc_label": "Automatiškai išplėsti vaizdo įrašo aprašymą: ",
|
"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_category_visual": "Vizualinės nuostatos",
|
||||||
"preferences_player_style_label": "Vaizdo grotuvo stilius: ",
|
"preferences_player_style_label": "Vaizdo grotuvo stilius: ",
|
||||||
"Dark mode: ": "Tamsus rėžimas: ",
|
"Dark mode: ": "Tamsus rėžimas: ",
|
||||||
|
@ -153,7 +153,7 @@
|
||||||
"Shared `x`": "Pasidalino `x`",
|
"Shared `x`": "Pasidalino `x`",
|
||||||
"Premieres in `x`": "Premjera už `x`",
|
"Premieres in `x`": "Premjera už `x`",
|
||||||
"Premieres `x`": "Premjera`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 YouTube comments": "Žiūrėti YouTube komentarus",
|
||||||
"View more comments on Reddit": "Žiūrėti daugiau komentarų Reddit",
|
"View more comments on Reddit": "Žiūrėti daugiau komentarų Reddit",
|
||||||
"View `x` comments": {
|
"View `x` comments": {
|
||||||
|
@ -371,5 +371,122 @@
|
||||||
"preferences_quality_dash_option_best": "Geriausia",
|
"preferences_quality_dash_option_best": "Geriausia",
|
||||||
"preferences_quality_dash_option_worst": "Blogiausia",
|
"preferences_quality_dash_option_worst": "Blogiausia",
|
||||||
"preferences_quality_dash_option_auto": "Automatinis",
|
"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_date_label": "Opplastningsdato",
|
||||||
"search_filters_apply_button": "Bruk valgte filtre",
|
"search_filters_apply_button": "Bruk valgte filtre",
|
||||||
"search_filters_date_option_none": "Siden begynnelsen",
|
"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>"
|
||||||
}
|
}
|
||||||
|
|
|
@ -341,5 +341,10 @@
|
||||||
"search_filters_features_option_location": "vị trí",
|
"search_filters_features_option_location": "vị trí",
|
||||||
"search_filters_features_option_hdr": "hdr",
|
"search_filters_features_option_hdr": "hdr",
|
||||||
"Current version: ": "Phiên bản hiện tại: ",
|
"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
|
Spectator.describe "Helper" do
|
||||||
describe "#produce_channel_videos_url" do
|
describe "#produce_channel_videos_url" do
|
||||||
it "correctly produces url for requesting page `x` of a channel's videos" 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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -172,6 +172,8 @@ end
|
||||||
CONNECTION_CHANNEL = Channel({Bool, Channel(PQ::Notification)}).new(32)
|
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::NotificationJob.new(CONNECTION_CHANNEL, CONFIG.database_url)
|
||||||
|
|
||||||
|
Invidious::Jobs.register Invidious::Jobs::ClearExpiredItemsJob.new
|
||||||
|
|
||||||
Invidious::Jobs.start_all
|
Invidious::Jobs.start_all
|
||||||
|
|
||||||
def popular_videos
|
def popular_videos
|
||||||
|
|
|
@ -1,53 +1,48 @@
|
||||||
def produce_channel_videos_continuation(ucid, page = 1, auto_generated = nil, sort_by = "newest", v2 = false)
|
def produce_channel_videos_continuation(ucid, page = 1, auto_generated = nil, sort_by = "newest", v2 = false)
|
||||||
object = {
|
object_inner_2 = {
|
||||||
"80226972:embedded" => {
|
"2:0:embedded" => {
|
||||||
"2:string" => ucid,
|
"1:0:varint" => 0_i64,
|
||||||
"3:base64" => {
|
},
|
||||||
"2:string" => "videos",
|
"5:varint" => 50_i64,
|
||||||
"6:varint" => 2_i64,
|
"6:varint" => 1_i64,
|
||||||
"7:varint" => 1_i64,
|
"7:varint" => (page * 30).to_i64,
|
||||||
"12:varint" => 1_i64,
|
"9:varint" => 1_i64,
|
||||||
"13:string" => "",
|
"10:varint" => 0_i64,
|
||||||
"23: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
|
object_inner_1_encoded = object_inner_1
|
||||||
if auto_generated
|
.try { |i| Protodec::Any.cast_json(i) }
|
||||||
seed = Time.unix(1525757349)
|
.try { |i| Protodec::Any.from_json(i) }
|
||||||
until seed >= Time.utc
|
.try { |i| Base64.urlsafe_encode(i) }
|
||||||
seed += 1.month
|
.try { |i| URI.encode_www_form(i) }
|
||||||
end
|
|
||||||
timestamp = seed - (page - 1).months
|
|
||||||
|
|
||||||
object["80226972:embedded"]["3:base64"].as(Hash)["4:varint"] = 0x36_i64
|
object = {
|
||||||
object["80226972:embedded"]["3:base64"].as(Hash)["15:string"] = "#{timestamp.to_unix}"
|
"80226972:embedded" => {
|
||||||
else
|
"2:string" => ucid,
|
||||||
object["80226972:embedded"]["3:base64"].as(Hash)["4:varint"] = 0_i64
|
"3:string" => object_inner_1_encoded,
|
||||||
object["80226972:embedded"]["3:base64"].as(Hash)["15:string"] = "#{page}"
|
"35:string" => "browse-feed#{ucid}videos102",
|
||||||
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")
|
|
||||||
|
|
||||||
continuation = object.try { |i| Protodec::Any.cast_json(i) }
|
continuation = object.try { |i| Protodec::Any.cast_json(i) }
|
||||||
.try { |i| Protodec::Any.from_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")
|
def get_60_videos(ucid, author, page, auto_generated, sort_by = "newest")
|
||||||
videos = [] of SearchVideo
|
videos = [] of SearchVideo
|
||||||
|
|
||||||
2.times do |i|
|
# 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, page * 2 + (i - 1), auto_generated: auto_generated, sort_by: sort_by)
|
||||||
videos.concat extract_videos(initial_data, author, ucid)
|
initial_data = get_channel_videos_response(ucid, 1, auto_generated: auto_generated, sort_by: sort_by)
|
||||||
end
|
videos = extract_videos(initial_data, author, ucid)
|
||||||
|
# end
|
||||||
|
|
||||||
return videos.size, videos
|
return videos.size, videos
|
||||||
end
|
end
|
||||||
|
|
|
@ -78,6 +78,10 @@ class Config
|
||||||
property decrypt_polling : Bool = false
|
property decrypt_polling : Bool = false
|
||||||
# Used for crawling channels: threads should check all videos uploaded by a channel
|
# Used for crawling channels: threads should check all videos uploaded by a channel
|
||||||
property full_refresh : Bool = false
|
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://
|
# Used to tell Invidious it is behind a proxy, so links to resources should be https://
|
||||||
property https_only : Bool?
|
property https_only : Bool?
|
||||||
# HMAC signing key for CSRF tokens and verifying pubsub subscriptions
|
# HMAC signing key for CSRF tokens and verifying pubsub subscriptions
|
||||||
|
|
|
@ -4,7 +4,7 @@ module Invidious::Database::Nonces
|
||||||
extend self
|
extend self
|
||||||
|
|
||||||
# -------------------
|
# -------------------
|
||||||
# Insert
|
# Insert / Delete
|
||||||
# -------------------
|
# -------------------
|
||||||
|
|
||||||
def insert(nonce : String, expire : Time)
|
def insert(nonce : String, expire : Time)
|
||||||
|
@ -17,6 +17,15 @@ module Invidious::Database::Nonces
|
||||||
PG_DB.exec(request, nonce, expire)
|
PG_DB.exec(request, nonce, expire)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def delete_expired
|
||||||
|
request = <<-SQL
|
||||||
|
DELETE FROM nonces *
|
||||||
|
WHERE expire < now()
|
||||||
|
SQL
|
||||||
|
|
||||||
|
PG_DB.exec(request)
|
||||||
|
end
|
||||||
|
|
||||||
# -------------------
|
# -------------------
|
||||||
# Update
|
# Update
|
||||||
# -------------------
|
# -------------------
|
||||||
|
|
|
@ -22,6 +22,15 @@ module Invidious::Database::Videos
|
||||||
PG_DB.exec(request, id)
|
PG_DB.exec(request, id)
|
||||||
end
|
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)
|
def update(video : Video)
|
||||||
request = <<-SQL
|
request = <<-SQL
|
||||||
UPDATE videos
|
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 = {
|
LOCALES_LIST = {
|
||||||
"ar" => "العربية", # Arabic
|
"ar" => "العربية", # Arabic
|
||||||
|
"bn" => "বাংলা", # Bengali
|
||||||
|
"ca" => "Català", # Catalan
|
||||||
"cs" => "Čeština", # Czech
|
"cs" => "Čeština", # Czech
|
||||||
"da" => "Dansk", # Danish
|
"da" => "Dansk", # Danish
|
||||||
"de" => "Deutsch", # German
|
"de" => "Deutsch", # German
|
||||||
|
@ -11,6 +10,7 @@ LOCALES_LIST = {
|
||||||
"eo" => "Esperanto", # Esperanto
|
"eo" => "Esperanto", # Esperanto
|
||||||
"es" => "Español", # Spanish
|
"es" => "Español", # Spanish
|
||||||
"et" => "Eesti keel", # Estonian
|
"et" => "Eesti keel", # Estonian
|
||||||
|
"eu" => "Euskara", # Basque
|
||||||
"fa" => "فارسی", # Persian
|
"fa" => "فارسی", # Persian
|
||||||
"fi" => "Suomi", # Finnish
|
"fi" => "Suomi", # Finnish
|
||||||
"fr" => "Français", # French
|
"fr" => "Français", # French
|
||||||
|
@ -32,6 +32,8 @@ LOCALES_LIST = {
|
||||||
"pt-PT" => "Português de Portugal", # Portuguese (Portugal)
|
"pt-PT" => "Português de Portugal", # Portuguese (Portugal)
|
||||||
"ro" => "Română", # Romanian
|
"ro" => "Română", # Romanian
|
||||||
"ru" => "Русский", # Russian
|
"ru" => "Русский", # Russian
|
||||||
|
"si" => "සිංහල", # Sinhala
|
||||||
|
"sk" => "Slovenčina", # Slovak
|
||||||
"sl" => "Slovenščina", # Slovenian
|
"sl" => "Slovenščina", # Slovenian
|
||||||
"sq" => "Shqip", # Albanian
|
"sq" => "Shqip", # Albanian
|
||||||
"sr" => "Srpski (latinica)", # Serbian (Latin)
|
"sr" => "Srpski (latinica)", # Serbian (Latin)
|
||||||
|
|
|
@ -1,12 +1,39 @@
|
||||||
module Invidious::Jobs
|
module Invidious::Jobs
|
||||||
JOBS = [] of BaseJob
|
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)
|
def self.register(job : BaseJob)
|
||||||
JOBS << job
|
JOBS << job
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.start_all
|
def self.start_all
|
||||||
JOBS.each do |job|
|
JOBS.each do |job|
|
||||||
|
# Don't run the main rountine if the job is disabled by config
|
||||||
|
next if job.disabled?
|
||||||
|
|
||||||
spawn { job.begin }
|
spawn { job.begin }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,3 +1,33 @@
|
||||||
abstract class Invidious::Jobs::BaseJob
|
abstract class Invidious::Jobs::BaseJob
|
||||||
abstract def begin
|
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
|
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 %>
|
<% ucid = channel.ucid %>
|
||||||
<% author = HTML.escape(channel.author) %>
|
<% author = HTML.escape(channel.author) %>
|
||||||
|
<% channel_profile_pic = URI.parse(channel.author_thumbnail).request_target %>
|
||||||
|
|
||||||
<% content_for "header" do %>
|
<% 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>
|
<title><%= author %> - Invidious</title>
|
||||||
<link rel="alternate" type="application/rss+xml" title="RSS" href="/feed/channel/<%= ucid %>" />
|
<link rel="alternate" type="application/rss+xml" title="RSS" href="/feed/channel/<%= ucid %>" />
|
||||||
<% end %>
|
<% end %>
|
||||||
|
@ -19,7 +32,7 @@
|
||||||
<div class="pure-g h-box">
|
<div class="pure-g h-box">
|
||||||
<div class="pure-u-2-3">
|
<div class="pure-u-2-3">
|
||||||
<div class="channel-profile">
|
<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 %>
|
<span><%= author %></span><% if !channel.verified.nil? && channel.verified %> <i class="icon ion ion-md-checkmark-circle"></i><% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
<meta name="thumbnail" content="<%= thumbnail %>">
|
<meta name="thumbnail" content="<%= thumbnail %>">
|
||||||
<meta name="description" content="<%= HTML.escape(video.short_description) %>">
|
<meta name="description" content="<%= HTML.escape(video.short_description) %>">
|
||||||
<meta name="keywords" content="<%= video.keywords.join(",") %>">
|
<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:url" content="<%= HOST_URL %>/watch?v=<%= video.id %>">
|
||||||
<meta property="og:title" content="<%= title %>">
|
<meta property="og:title" content="<%= title %>">
|
||||||
<meta property="og:image" content="/vi/<%= video.id %>/maxres.jpg">
|
<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:width" content="1280">
|
||||||
<meta property="og:video:height" content="720">
|
<meta property="og:video:height" content="720">
|
||||||
<meta name="twitter:card" content="player">
|
<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:url" content="<%= HOST_URL %>/watch?v=<%= video.id %>">
|
||||||
<meta name="twitter:title" content="<%= title %>">
|
<meta name="twitter:title" content="<%= title %>">
|
||||||
<meta name="twitter:description" content="<%= HTML.escape(video.short_description) %>">
|
<meta name="twitter:description" content="<%= HTML.escape(video.short_description) %>">
|
||||||
|
|
|
@ -17,6 +17,7 @@ private ITEM_PARSERS = {
|
||||||
Parsers::PlaylistRendererParser,
|
Parsers::PlaylistRendererParser,
|
||||||
Parsers::CategoryRendererParser,
|
Parsers::CategoryRendererParser,
|
||||||
Parsers::RichItemRendererParser,
|
Parsers::RichItemRendererParser,
|
||||||
|
Parsers::ReelItemRendererParser,
|
||||||
}
|
}
|
||||||
|
|
||||||
record AuthorFallback, name : String, id : String
|
record AuthorFallback, name : String, id : String
|
||||||
|
@ -369,7 +370,7 @@ private module Parsers
|
||||||
end
|
end
|
||||||
|
|
||||||
# Parses an InnerTube richItemRenderer into a SearchVideo.
|
# 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
|
# A richItemRenderer seems to be a simple wrapper for a videoRenderer, used
|
||||||
# by the result page for hashtags. It is located inside a continuationItems
|
# by the result page for hashtags. It is located inside a continuationItems
|
||||||
|
@ -390,6 +391,90 @@ private module Parsers
|
||||||
return {{@type.name}}
|
return {{@type.name}}
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
|
||||||
# The following are the extractors for extracting an array of items from
|
# The following are the extractors for extracting an array of items from
|
||||||
|
@ -436,7 +521,18 @@ private module Extractors
|
||||||
content = extract_selected_tab(target["tabs"])["content"]
|
content = extract_selected_tab(target["tabs"])["content"]
|
||||||
|
|
||||||
if section_list_contents = content.dig?("sectionListRenderer", "contents")
|
if section_list_contents = content.dig?("sectionListRenderer", "contents")
|
||||||
section_list_contents.as_a.each do |renderer_container|
|
raw_items = unpack_section_list(section_list_contents)
|
||||||
|
elsif rich_grid_contents = content.dig?("richGridRenderer", "contents")
|
||||||
|
raw_items = rich_grid_contents.as_a
|
||||||
|
end
|
||||||
|
|
||||||
|
return raw_items
|
||||||
|
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]
|
renderer_container_contents = renderer_container["itemSectionRenderer"]["contents"][0]
|
||||||
|
|
||||||
# Category extraction
|
# Category extraction
|
||||||
|
@ -452,7 +548,6 @@ private module Extractors
|
||||||
raw_items << item
|
raw_items << item
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
return raw_items
|
return raw_items
|
||||||
end
|
end
|
||||||
|
@ -525,14 +620,11 @@ private module Extractors
|
||||||
end
|
end
|
||||||
|
|
||||||
private def self.extract(target)
|
private def self.extract(target)
|
||||||
raw_items = [] of JSON::Any
|
content = target["continuationItems"]?
|
||||||
if content = target["gridContinuation"]?
|
content ||= target.dig?("gridContinuation", "items")
|
||||||
raw_items = content["items"].as_a
|
content ||= target.dig?("richGridContinuation", "contents")
|
||||||
elsif content = target["continuationItems"]?
|
|
||||||
raw_items = content.as_a
|
|
||||||
end
|
|
||||||
|
|
||||||
return raw_items
|
return content.nil? ? [] of JSON::Any : content.as_a
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.extractor_name
|
def self.extractor_name
|
||||||
|
|
読み込み中…
新しいイシューから参照