├── locales ├── af.json ├── or.json └── bn_BD.json ├── kubernetes ├── .gitignore ├── Chart.lock ├── templates │ ├── configmap.yaml │ ├── _helpers.tpl │ ├── service.yaml │ ├── hpa.yaml │ └── deployment.yaml ├── Chart.yaml ├── README.md └── values.yaml ├── assets ├── robots.txt ├── favicon.ico ├── favicon-16x16.png ├── favicon-32x32.png ├── fonts │ ├── ionicons.eot │ ├── ionicons.ttf │ ├── ionicons.woff │ └── ionicons.woff2 ├── mstile-150x150.png ├── apple-touch-icon.png ├── videojs │ └── .gitignore ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── browserconfig.xml ├── css │ ├── empty.css │ ├── quality-selector.css │ ├── embed.css │ ├── videojs-youtube-annotations.min.css │ └── search.css ├── site.webmanifest ├── js │ ├── watched_indicator.js │ ├── watched_widget.js │ ├── playlist_widget.js │ ├── themes.js │ ├── subscribe_widget.js │ ├── embed.js │ └── community.js └── safari-pinned-tab.svg ├── .github ├── FUNDING.yml ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── enhancement.md │ ├── feature_request.md │ └── bug_report.md └── workflows │ ├── stale.yml │ ├── auto-close-duplicate.yaml │ └── ci.yml ├── TRANSLATION ├── .gitmodules ├── screenshots ├── 01_player.png ├── 02_preferences.png ├── 04_description.png ├── 05_preferences.png ├── 03_subscriptions.png ├── 06_subscriptions.png └── native_notification.png ├── .gitignore ├── .gitattributes ├── .editorconfig ├── src ├── invidious │ ├── views │ │ ├── error.ecr │ │ ├── message.ecr │ │ ├── components │ │ │ ├── search_box.ecr │ │ │ ├── feed_menu.ecr │ │ │ ├── video-context-buttons.ecr │ │ │ ├── player_sources.ecr │ │ │ ├── subscribe_widget.ecr │ │ │ └── channel_info.ecr │ │ ├── mix.ecr │ │ ├── feeds │ │ │ ├── popular.ecr │ │ │ ├── playlists.ecr │ │ │ ├── trending.ecr │ │ │ ├── subscriptions.ecr │ │ │ └── history.ecr │ │ ├── search_homepage.ecr │ │ ├── user │ │ │ ├── delete_account.ecr │ │ │ ├── clear_watch_history.ecr │ │ │ ├── change_password.ecr │ │ │ ├── token_manager.ecr │ │ │ ├── subscription_manager.ecr │ │ │ └── authorize_token.ecr │ │ ├── delete_playlist.ecr │ │ ├── hashtag.ecr │ │ ├── embed.ecr │ │ ├── community.ecr │ │ ├── create_playlist.ecr │ │ ├── search.ecr │ │ ├── channel.ecr │ │ └── add_playlist_items.ecr │ ├── user │ │ ├── converters.cr │ │ ├── user.cr │ │ ├── cookies.cr │ │ └── exports.cr │ ├── database │ │ ├── migrations │ │ │ ├── 0010_make_videos_unlogged.cr │ │ │ ├── 0007_create_annotations_table.cr │ │ │ ├── 0006_create_nonces_table.cr │ │ │ ├── 0009_create_playlist_videos_table.cr │ │ │ ├── 0002_create_videos_table.cr │ │ │ ├── 0005_create_session_ids_table.cr │ │ │ ├── 0001_create_channels_table.cr │ │ │ ├── 0004_create_users_table.cr │ │ │ ├── 0003_create_channel_videos_table.cr │ │ │ └── 0008_create_playlists_table.cr │ │ ├── annotations.cr │ │ ├── migration.cr │ │ ├── statistics.cr │ │ ├── videos.cr │ │ ├── nonces.cr │ │ ├── migrator.cr │ │ └── sessions.cr │ ├── videos │ │ ├── music.cr │ │ ├── regions.cr │ │ └── description.cr │ ├── jobs │ │ ├── update_decrypt_function_job.cr │ │ ├── pull_popular_videos_job.cr │ │ ├── notification_job.cr │ │ ├── clear_expired_items_job.cr │ │ ├── base_job.cr │ │ ├── subscribe_to_feeds_job.cr │ │ ├── statistics_refresh_job.cr │ │ ├── refresh_channels_job.cr │ │ └── refresh_feeds_job.cr │ ├── frontend │ │ ├── misc.cr │ │ ├── channel_page.cr │ │ └── comments_reddit.cr │ ├── jsonify │ │ └── api_v1 │ │ │ └── common.cr │ ├── http_server │ │ └── utils.cr │ ├── search │ │ ├── ctoken.cr │ │ └── processors.cr │ ├── routes │ │ ├── notifications.cr │ │ ├── api │ │ │ └── v1 │ │ │ │ ├── feeds.cr │ │ │ │ └── search.cr │ │ ├── misc.cr │ │ └── errors.cr │ ├── jobs.cr │ ├── channels │ │ └── playlists.cr │ ├── exceptions.cr │ ├── trending.cr │ ├── hashtag.cr │ ├── comments │ │ ├── reddit_types.cr │ │ ├── reddit.cr │ │ └── links_util.cr │ ├── helpers │ │ ├── logger.cr │ │ ├── crystal_class_overrides.cr │ │ ├── macros.cr │ │ └── signatures.cr │ └── yt_backend │ │ └── extractors_utils.cr └── ext │ └── kemal_content_for.cr ├── config ├── migrate-scripts │ ├── migrate-db-3bcb98e.sh │ ├── migrate-db-52cb239.sh │ ├── migrate-db-701b5ea.sh │ ├── migrate-db-88b7097.sh │ ├── migrate-db-30e6d29.sh │ ├── migrate-db-17cf077.sh │ ├── migrate-db-6e51189.sh │ ├── migrate-db-8e884fe.sh │ ├── migrate-db-3646395.sh │ ├── migrate-db-1c8075c.sh │ └── migrate-db-1eca969.sh └── sql │ ├── annotations.sql │ ├── playlist_videos.sql │ ├── nonces.sql │ ├── videos.sql │ ├── session_ids.sql │ ├── playlists.sql │ ├── channels.sql │ ├── users.sql │ └── channel_videos.sql ├── invidious.service ├── spec ├── spec_helper.cr ├── parsers_helper.cr └── invidious │ ├── user │ └── imports_spec.cr │ └── utils_spec.cr ├── scripts ├── git │ └── pre-commit └── deploy-database.sh ├── docker ├── init-invidious-db.sh ├── Dockerfile └── Dockerfile.arm64 ├── shard.yml ├── shard.lock ├── videojs-dependencies.yml ├── docker-compose.yml ├── .ameba.yml └── Makefile /locales/af.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /locales/or.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /kubernetes/.gitignore: -------------------------------------------------------------------------------- 1 | /charts/*.tgz 2 | -------------------------------------------------------------------------------- /assets/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: / 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: https://invidious.io/donate/ 2 | -------------------------------------------------------------------------------- /TRANSLATION: -------------------------------------------------------------------------------- 1 | https://hosted.weblate.org/projects/invidious/ 2 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "mocks"] 2 | path = mocks 3 | url = ../mocks 4 | -------------------------------------------------------------------------------- /assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/g3gg0/invidious/master/assets/favicon.ico -------------------------------------------------------------------------------- /assets/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/g3gg0/invidious/master/assets/favicon-16x16.png -------------------------------------------------------------------------------- /assets/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/g3gg0/invidious/master/assets/favicon-32x32.png -------------------------------------------------------------------------------- /assets/fonts/ionicons.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/g3gg0/invidious/master/assets/fonts/ionicons.eot -------------------------------------------------------------------------------- /assets/fonts/ionicons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/g3gg0/invidious/master/assets/fonts/ionicons.ttf -------------------------------------------------------------------------------- /assets/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/g3gg0/invidious/master/assets/mstile-150x150.png -------------------------------------------------------------------------------- /screenshots/01_player.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/g3gg0/invidious/master/screenshots/01_player.png -------------------------------------------------------------------------------- /assets/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/g3gg0/invidious/master/assets/apple-touch-icon.png -------------------------------------------------------------------------------- /assets/fonts/ionicons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/g3gg0/invidious/master/assets/fonts/ionicons.woff -------------------------------------------------------------------------------- /assets/fonts/ionicons.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/g3gg0/invidious/master/assets/fonts/ionicons.woff2 -------------------------------------------------------------------------------- /assets/videojs/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore everything in this directory 2 | * 3 | # Except this file 4 | !.gitignore 5 | -------------------------------------------------------------------------------- /screenshots/02_preferences.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/g3gg0/invidious/master/screenshots/02_preferences.png -------------------------------------------------------------------------------- /screenshots/04_description.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/g3gg0/invidious/master/screenshots/04_description.png -------------------------------------------------------------------------------- /screenshots/05_preferences.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/g3gg0/invidious/master/screenshots/05_preferences.png -------------------------------------------------------------------------------- /screenshots/03_subscriptions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/g3gg0/invidious/master/screenshots/03_subscriptions.png -------------------------------------------------------------------------------- /screenshots/06_subscriptions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/g3gg0/invidious/master/screenshots/06_subscriptions.png -------------------------------------------------------------------------------- /assets/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/g3gg0/invidious/master/assets/android-chrome-192x192.png -------------------------------------------------------------------------------- /assets/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/g3gg0/invidious/master/assets/android-chrome-512x512.png -------------------------------------------------------------------------------- /screenshots/native_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/g3gg0/invidious/master/screenshots/native_notification.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /docs/ 2 | /dev/ 3 | /lib/ 4 | /bin/ 5 | /.shards/ 6 | /.vscode/ 7 | /invidious 8 | /sentry 9 | /config/config.yml 10 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # https://github.community/t/how-to-change-the-category/2261/3 2 | videojs-*.js linguist-detectable=false 3 | video.min.js linguist-detectable=false 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.cr] 2 | charset = utf-8 3 | end_of_line = lf 4 | insert_final_newline = true 5 | indent_style = space 6 | indent_size = 2 7 | trim_trailing_whitespace = true 8 | -------------------------------------------------------------------------------- /src/invidious/views/error.ecr: -------------------------------------------------------------------------------- 1 | <% content_for "header" do %> 2 |
11 | <%= message %> 12 |
13 | -------------------------------------------------------------------------------- /config/sql/annotations.sql: -------------------------------------------------------------------------------- 1 | -- Table: public.annotations 2 | 3 | -- DROP TABLE public.annotations; 4 | 5 | CREATE TABLE IF NOT EXISTS public.annotations 6 | ( 7 | id text NOT NULL, 8 | annotations xml, 9 | CONSTRAINT annotations_id_key UNIQUE (id) 10 | ); 11 | 12 | GRANT ALL ON TABLE public.annotations TO current_user; 13 | -------------------------------------------------------------------------------- /src/invidious/database/migrations/0010_make_videos_unlogged.cr: -------------------------------------------------------------------------------- 1 | module Invidious::Database::Migrations 2 | class MakeVideosUnlogged < Migration 3 | version 10 4 | 5 | def up(conn : DB::Connection) 6 | conn.exec <<-SQL 7 | ALTER TABLE public.videos SET UNLOGGED; 8 | SQL 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /config/migrate-scripts/migrate-db-30e6d29.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | [ -z "$POSTGRES_USER" ] && POSTGRES_USER=kemal 4 | [ -z "$POSTGRES_DB" ] && POSTGRES_DB=invidious 5 | 6 | psql "$POSTGRES_DB" "$POSTGRES_USER" -c "ALTER TABLE channels ADD COLUMN deleted bool;" 7 | psql "$POSTGRES_DB" "$POSTGRES_USER" -c "UPDATE channels SET deleted = false;" 8 | -------------------------------------------------------------------------------- /config/migrate-scripts/migrate-db-17cf077.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | [ -z "$POSTGRES_USER" ] && POSTGRES_USER=kemal 4 | [ -z "$POSTGRES_DB" ] && POSTGRES_DB=invidious 5 | 6 | psql "$POSTGRES_DB" "$POSTGRES_USER" -c "ALTER TABLE channels ADD COLUMN subscribed bool;" 7 | psql "$POSTGRES_DB" "$POSTGRES_USER" -c "UPDATE channels SET subscribed = false;" 8 | -------------------------------------------------------------------------------- /assets/css/empty.css: -------------------------------------------------------------------------------- 1 | #search-widget { 2 | text-align: center; 3 | margin: 20vh 0 50px 0; 4 | } 5 | 6 | #logo > h1 { 7 | font-size: 3.5em; 8 | margin: 0; 9 | padding: 0; 10 | } 11 | 12 | @media screen and (max-width: 1500px) and (max-height: 1000px) { 13 | #logo > h1 { 14 | font-size: 10vmin; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /config/migrate-scripts/migrate-db-6e51189.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | [ -z "$POSTGRES_USER" ] && POSTGRES_USER=kemal 4 | [ -z "$POSTGRES_DB" ] && POSTGRES_DB=invidious 5 | 6 | psql "$POSTGRES_DB" "$POSTGRES_USER" -c "ALTER TABLE channel_videos ADD COLUMN live_now bool;" 7 | psql "$POSTGRES_DB" "$POSTGRES_USER" -c "UPDATE channel_videos SET live_now = false;" 8 | -------------------------------------------------------------------------------- /src/invidious/videos/music.cr: -------------------------------------------------------------------------------- 1 | require "json" 2 | 3 | struct VideoMusic 4 | include JSON::Serializable 5 | 6 | property song : String 7 | property album : String 8 | property artist : String 9 | property license : String 10 | 11 | def initialize(@song : String, @album : String, @artist : String, @license : String) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /kubernetes/templates/configmap.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: {{ template "invidious.fullname" . }} 5 | labels: 6 | app: {{ template "invidious.name" . }} 7 | chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" 8 | release: {{ .Release.Name }} 9 | data: 10 | INVIDIOUS_CONFIG: | 11 | {{ toYaml .Values.config | indent 4 }} 12 | -------------------------------------------------------------------------------- /invidious.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Invidious (An alternative YouTube front-end) 3 | After=syslog.target 4 | After=network.target 5 | 6 | [Service] 7 | RestartSec=2s 8 | Type=simple 9 | 10 | User=invidious 11 | Group=invidious 12 | 13 | WorkingDirectory=/home/invidious/invidious 14 | ExecStart=/home/invidious/invidious/invidious -o invidious.log 15 | 16 | Restart=always 17 | 18 | [Install] 19 | WantedBy=multi-user.target 20 | -------------------------------------------------------------------------------- /src/invidious/jobs/update_decrypt_function_job.cr: -------------------------------------------------------------------------------- 1 | class Invidious::Jobs::UpdateDecryptFunctionJob < Invidious::Jobs::BaseJob 2 | def begin 3 | loop do 4 | begin 5 | DECRYPT_FUNCTION.update_decrypt_function 6 | rescue ex 7 | LOGGER.error("UpdateDecryptFunctionJob : #{ex.message}") 8 | ensure 9 | sleep 1.minute 10 | Fiber.yield 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /src/ext/kemal_content_for.cr: -------------------------------------------------------------------------------- 1 | # Overrides for Kemal's `content_for` macro in order to keep using 2 | # kilt as it was before Kemal v1.1.1 (Kemal PR #618). 3 | 4 | require "kemal" 5 | require "kilt" 6 | 7 | macro content_for(key, file = __FILE__) 8 | %proc = ->() { 9 | __kilt_io__ = IO::Memory.new 10 | {{ yield }} 11 | __kilt_io__.to_s 12 | } 13 | 14 | CONTENT_FOR_BLOCKS[{{key}}] = Tuple.new {{file}}, %proc 15 | nil 16 | end 17 | -------------------------------------------------------------------------------- /config/migrate-scripts/migrate-db-8e884fe.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | [ -z "$POSTGRES_USER" ] && POSTGRES_USER=kemal 4 | [ -z "$POSTGRES_DB" ] && POSTGRES_DB=invidious 5 | 6 | psql "$POSTGRES_DB" "$POSTGRES_USER" -c "ALTER TABLE channels DROP COLUMN subscribed" 7 | psql "$POSTGRES_DB" "$POSTGRES_USER" -c "ALTER TABLE channels ADD COLUMN subscribed timestamptz" 8 | psql "$POSTGRES_DB" "$POSTGRES_USER" -c "UPDATE channels SET subscribed = '2019-01-01 00:00:00+00'" 9 | -------------------------------------------------------------------------------- /config/migrate-scripts/migrate-db-3646395.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | [ -z "$POSTGRES_USER" ] && POSTGRES_USER=kemal 4 | [ -z "$POSTGRES_DB" ] && POSTGRES_DB=invidious 5 | 6 | psql "$POSTGRES_DB" "$POSTGRES_USER" < config/sql/session_ids.sql 7 | psql "$POSTGRES_DB" "$POSTGRES_USER" -c "INSERT INTO session_ids (SELECT unnest(id), email, CURRENT_TIMESTAMP FROM users) ON CONFLICT (id) DO NOTHING" 8 | psql "$POSTGRES_DB" "$POSTGRES_USER" -c "ALTER TABLE users DROP COLUMN id" 9 | -------------------------------------------------------------------------------- /assets/css/quality-selector.css: -------------------------------------------------------------------------------- 1 | .vjs-quality-selector .vjs-menu-button{margin:0;padding:0;height:100%;width:100%}.vjs-quality-selector .vjs-icon-placeholder{font-family:'VideoJS';font-weight:normal;font-style:normal}.vjs-quality-selector .vjs-icon-placeholder:before{content:'\f110'}.vjs-quality-changing .vjs-big-play-button{display:none}.vjs-quality-changing .vjs-control-bar{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;visibility:visible;opacity:1} 2 | -------------------------------------------------------------------------------- /src/invidious/views/components/search_box.ecr: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /src/invidious/frontend/misc.cr: -------------------------------------------------------------------------------- 1 | module Invidious::Frontend::Misc 2 | extend self 3 | 4 | def redirect_url(env : HTTP::Server::Context) 5 | prefs = env.get("preferences").as(Preferences) 6 | 7 | if prefs.automatic_instance_redirect 8 | current_page = env.get?("current_page").as(String) 9 | redirect_url = "/redirect?referer=#{current_page}" 10 | else 11 | redirect_url = "https://redirect.invidious.io#{env.request.resource}" 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /src/invidious/views/components/feed_menu.ecr: -------------------------------------------------------------------------------- 1 | 12 | -------------------------------------------------------------------------------- /config/sql/playlist_videos.sql: -------------------------------------------------------------------------------- 1 | -- Table: public.playlist_videos 2 | 3 | -- DROP TABLE public.playlist_videos; 4 | 5 | CREATE TABLE IF NOT EXISTS public.playlist_videos 6 | ( 7 | title text, 8 | id text, 9 | author text, 10 | ucid text, 11 | length_seconds integer, 12 | published timestamptz, 13 | plid text references playlists(id), 14 | index int8, 15 | live_now boolean, 16 | PRIMARY KEY (index,plid) 17 | ); 18 | 19 | GRANT ALL ON TABLE public.playlist_videos TO current_user; 20 | -------------------------------------------------------------------------------- /assets/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Invidious", 3 | "short_name": "Invidious", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#575757", 17 | "background_color": "#575757", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /spec/spec_helper.cr: -------------------------------------------------------------------------------- 1 | require "kemal" 2 | require "openssl/hmac" 3 | require "pg" 4 | require "protodec/utils" 5 | require "yaml" 6 | require "../src/invidious/helpers/*" 7 | require "../src/invidious/channels/*" 8 | require "../src/invidious/videos/caption" 9 | require "../src/invidious/videos" 10 | require "../src/invidious/playlists" 11 | require "../src/invidious/search/ctoken" 12 | require "../src/invidious/trending" 13 | require "spectator" 14 | 15 | Spectator.configure do |config| 16 | config.fail_blank 17 | config.randomize 18 | end 19 | -------------------------------------------------------------------------------- /config/sql/nonces.sql: -------------------------------------------------------------------------------- 1 | -- Table: public.nonces 2 | 3 | -- DROP TABLE public.nonces; 4 | 5 | CREATE TABLE IF NOT EXISTS public.nonces 6 | ( 7 | nonce text, 8 | expire timestamp with time zone, 9 | CONSTRAINT nonces_id_key UNIQUE (nonce) 10 | ); 11 | 12 | GRANT ALL ON TABLE public.nonces TO current_user; 13 | 14 | -- Index: public.nonces_nonce_idx 15 | 16 | -- DROP INDEX public.nonces_nonce_idx; 17 | 18 | CREATE INDEX IF NOT EXISTS nonces_nonce_idx 19 | ON public.nonces 20 | USING btree 21 | (nonce COLLATE pg_catalog."default"); 22 | 23 | -------------------------------------------------------------------------------- /src/invidious/jobs/pull_popular_videos_job.cr: -------------------------------------------------------------------------------- 1 | class Invidious::Jobs::PullPopularVideosJob < Invidious::Jobs::BaseJob 2 | POPULAR_VIDEOS = Atomic.new([] of ChannelVideo) 3 | private getter db : DB::Database 4 | 5 | def initialize(@db) 6 | end 7 | 8 | def begin 9 | loop do 10 | videos = Invidious::Database::ChannelVideos.select_popular_videos 11 | .sort_by!(&.published) 12 | .reverse! 13 | 14 | POPULAR_VIDEOS.set(videos) 15 | 16 | sleep 1.minute 17 | Fiber.yield 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /config/sql/videos.sql: -------------------------------------------------------------------------------- 1 | -- Table: public.videos 2 | 3 | -- DROP TABLE public.videos; 4 | 5 | CREATE UNLOGGED TABLE IF NOT EXISTS public.videos 6 | ( 7 | id text NOT NULL, 8 | info text, 9 | updated timestamp with time zone, 10 | CONSTRAINT videos_pkey PRIMARY KEY (id) 11 | ); 12 | 13 | GRANT ALL ON TABLE public.videos TO current_user; 14 | 15 | -- Index: public.id_idx 16 | 17 | -- DROP INDEX public.id_idx; 18 | 19 | CREATE UNIQUE INDEX IF NOT EXISTS id_idx 20 | ON public.videos 21 | USING btree 22 | (id COLLATE pg_catalog."default"); 23 | 24 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Default and lowest precedence. If none of the below matches, @iv-org/developers would be requested for review. 2 | * @iv-org/developers 3 | 4 | docker-compose.yml @unixfox 5 | docker/ @unixfox 6 | kubernetes/ @unixfox 7 | 8 | README.md @thefrenchghosty 9 | config/config.example.yml @thefrenchghosty @SamantazFox @unixfox 10 | 11 | scripts/ @syeopite 12 | shards.lock @syeopite 13 | shards.yml @syeopite 14 | 15 | locales/ @SamantazFox 16 | src/invidious/helpers/i18n.cr @SamantazFox 17 | 18 | src/invidious/helpers/youtube_api.cr @SamantazFox 19 | -------------------------------------------------------------------------------- /assets/css/embed.css: -------------------------------------------------------------------------------- 1 | #player { 2 | position: fixed; 3 | right: 0; 4 | bottom: 0; 5 | min-width: 100%; 6 | min-height: 100%; 7 | width: auto; 8 | height: auto; 9 | z-index: -100; 10 | } 11 | 12 | .watch-on-invidious { 13 | font-size: 1.3em !important; 14 | font-weight: bold; 15 | white-space: nowrap; 16 | margin: 0 1em 0 1em !important; 17 | order: 3; 18 | } 19 | 20 | .watch-on-invidious > a { 21 | color: white; 22 | } 23 | 24 | .watch-on-invidious > a:hover, 25 | .watch-on-invidious > a:focus { 26 | color: rgba(0, 182, 240, 1);; 27 | } 28 | -------------------------------------------------------------------------------- /src/invidious/jsonify/api_v1/common.cr: -------------------------------------------------------------------------------- 1 | require "json" 2 | 3 | module Invidious::JSONify::APIv1 4 | extend self 5 | 6 | def thumbnails(json : JSON::Builder, id : String) 7 | json.array do 8 | build_thumbnails(id).each do |thumbnail| 9 | json.object do 10 | json.field "quality", thumbnail[:name] 11 | json.field "url", "#{thumbnail[:host]}/vi/#{id}/#{thumbnail["url"]}.jpg" 12 | json.field "width", thumbnail[:width] 13 | json.field "height", thumbnail[:height] 14 | end 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /config/migrate-scripts/migrate-db-1c8075c.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | [ -z "$POSTGRES_USER" ] && POSTGRES_USER=kemal 4 | [ -z "$POSTGRES_DB" ] && POSTGRES_DB=invidious 5 | 6 | psql "$POSTGRES_DB" "$POSTGRES_USER" -c "ALTER TABLE channel_videos DROP COLUMN live_now CASCADE" 7 | psql "$POSTGRES_DB" "$POSTGRES_USER" -c "ALTER TABLE channel_videos DROP COLUMN premiere_timestamp CASCADE" 8 | 9 | psql "$POSTGRES_DB" "$POSTGRES_USER" -c "ALTER TABLE channel_videos ADD COLUMN live_now bool" 10 | psql "$POSTGRES_DB" "$POSTGRES_USER" -c "ALTER TABLE channel_videos ADD COLUMN premiere_timestamp timestamptz" 11 | -------------------------------------------------------------------------------- /src/invidious/database/migrations/0007_create_annotations_table.cr: -------------------------------------------------------------------------------- 1 | module Invidious::Database::Migrations 2 | class CreateAnnotationsTable < Migration 3 | version 7 4 | 5 | def up(conn : DB::Connection) 6 | conn.exec <<-SQL 7 | CREATE TABLE IF NOT EXISTS public.annotations 8 | ( 9 | id text NOT NULL, 10 | annotations xml, 11 | CONSTRAINT annotations_id_key UNIQUE (id) 12 | ); 13 | SQL 14 | 15 | conn.exec <<-SQL 16 | GRANT ALL ON TABLE public.annotations TO current_user; 17 | SQL 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /config/sql/session_ids.sql: -------------------------------------------------------------------------------- 1 | -- Table: public.session_ids 2 | 3 | -- DROP TABLE public.session_ids; 4 | 5 | CREATE TABLE IF NOT EXISTS public.session_ids 6 | ( 7 | id text NOT NULL, 8 | email text, 9 | issued timestamp with time zone, 10 | CONSTRAINT session_ids_pkey PRIMARY KEY (id) 11 | ); 12 | 13 | GRANT ALL ON TABLE public.session_ids TO current_user; 14 | 15 | -- Index: public.session_ids_id_idx 16 | 17 | -- DROP INDEX public.session_ids_id_idx; 18 | 19 | CREATE INDEX IF NOT EXISTS session_ids_id_idx 20 | ON public.session_ids 21 | USING btree 22 | (id COLLATE pg_catalog."default"); 23 | 24 | -------------------------------------------------------------------------------- /src/invidious/database/annotations.cr: -------------------------------------------------------------------------------- 1 | require "./base.cr" 2 | 3 | module Invidious::Database::Annotations 4 | extend self 5 | 6 | def insert(id : String, annotations : String) 7 | request = <<-SQL 8 | INSERT INTO annotations 9 | VALUES ($1, $2) 10 | ON CONFLICT DO NOTHING 11 | SQL 12 | 13 | PG_DB.exec(request, id, annotations) 14 | end 15 | 16 | def select(id : String) : Annotation? 17 | request = <<-SQL 18 | SELECT * FROM annotations 19 | WHERE id = $1 20 | SQL 21 | 22 | return PG_DB.query_one?(request, id, as: Annotation) 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /src/invidious/views/mix.ecr: -------------------------------------------------------------------------------- 1 | <% content_for "header" do %> 2 |<%= error_message %>
27 |33 | [ − ] 34 | #{child.author} 35 | #{translate_count(locale, "comments_points_count", child.score, NumberFormatting::Separator)} 36 | #{translate(locale, "`x` ago", recode_date(child.created_utc, locale))} 37 | #{translate(locale, "permalink")} 38 |
39 |<%= token[:session] %>
25 | 4 |
10 | 11 | <% else %> 12 |13 |
19 | 20 | <% end %> 21 | 22 | 34 | 35 | <% else %> 36 |37 | "> 39 | <%= translate(locale, "Subscribe") %> | <%= sub_count_text %> 40 | 41 |
42 | <% end %> 43 | -------------------------------------------------------------------------------- /docker/Dockerfile.arm64: -------------------------------------------------------------------------------- 1 | FROM alpine:3.16 AS builder 2 | 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 3 | 4 | ARG release 5 | ARG disable_quic 6 | 7 | WORKDIR /invidious 8 | COPY ./shard.yml ./shard.yml 9 | COPY ./shard.lock ./shard.lock 10 | RUN shards install --production 11 | 12 | COPY --from=quay.io/invidious/lsquic-compiled /root/liblsquic.a ./lib/lsquic/src/lsquic/ext/liblsquic.a 13 | 14 | COPY ./src/ ./src/ 15 | # TODO: .git folder is required for building – this is destructive. 16 | # See definition of CURRENT_BRANCH, CURRENT_COMMIT and CURRENT_VERSION. 17 | COPY ./.git/ ./.git/ 18 | 19 | # Required for fetching player dependencies 20 | COPY ./scripts/ ./scripts/ 21 | COPY ./assets/ ./assets/ 22 | COPY ./videojs-dependencies.yml ./videojs-dependencies.yml 23 | 24 | RUN crystal spec --warnings all \ 25 | --link-flags "-lxml2 -llzma" 26 | 27 | RUN if [[ "${release}" == 1 && "${disable_quic}" == 1 ]] ; then \ 28 | crystal build ./src/invidious.cr \ 29 | --release \ 30 | -Ddisable_quic \ 31 | --static --warnings all \ 32 | --link-flags "-lxml2 -llzma"; \ 33 | elif [[ "${release}" == 1 ]] ; then \ 34 | crystal build ./src/invidious.cr \ 35 | --release \ 36 | --static --warnings all \ 37 | --link-flags "-lxml2 -llzma"; \ 38 | else \ 39 | crystal build ./src/invidious.cr \ 40 | --static --warnings all \ 41 | --link-flags "-lxml2 -llzma"; \ 42 | fi 43 | 44 | FROM alpine:3.16 45 | RUN apk add --no-cache librsvg ttf-opensans tini 46 | WORKDIR /invidious 47 | RUN addgroup -g 1000 -S invidious && \ 48 | adduser -u 1000 -S invidious -G invidious 49 | COPY --chown=invidious ./config/config.* ./config/ 50 | RUN mv -n config/config.example.yml config/config.yml 51 | RUN sed -i 's/host: \(127.0.0.1\|localhost\)/host: invidious-db/' config/config.yml 52 | COPY ./config/sql/ ./config/sql/ 53 | COPY ./locales/ ./locales/ 54 | COPY --from=builder /invidious/assets ./assets/ 55 | COPY --from=builder /invidious/invidious . 56 | RUN chmod o+rX -R ./assets ./config ./locales 57 | 58 | EXPOSE 3000 59 | USER invidious 60 | ENTRYPOINT ["/sbin/tini", "--"] 61 | CMD [ "/invidious/invidious" ] 62 | -------------------------------------------------------------------------------- /src/invidious/search/processors.cr: -------------------------------------------------------------------------------- 1 | module Invidious::Search 2 | module Processors 3 | extend self 4 | 5 | # Regular search (`/search` endpoint) 6 | def regular(query : Query) : Array(SearchItem) 7 | search_params = query.filters.to_yt_params(page: query.page) 8 | 9 | client_config = YoutubeAPI::ClientConfig.new(region: query.region) 10 | initial_data = YoutubeAPI.search(query.text, search_params, client_config: client_config) 11 | 12 | items, _ = extract_items(initial_data) 13 | return items.reject!(Category) 14 | end 15 | 16 | # Search a youtube channel 17 | # TODO: clean code, and rely more on YoutubeAPI 18 | def channel(query : Query) : Array(SearchItem) 19 | response = YT_POOL.client &.get("/channel/#{query.channel}") 20 | 21 | if response.status_code == 404 22 | response = YT_POOL.client &.get("/user/#{query.channel}") 23 | response = YT_POOL.client &.get("/c/#{query.channel}") if response.status_code == 404 24 | initial_data = extract_initial_data(response.body) 25 | ucid = initial_data.dig?("header", "c4TabbedHeaderRenderer", "channelId").try(&.as_s?) 26 | raise ChannelSearchException.new(query.channel) if !ucid 27 | else 28 | ucid = query.channel 29 | end 30 | 31 | continuation = produce_channel_search_continuation(ucid, query.text, query.page) 32 | response_json = YoutubeAPI.browse(continuation) 33 | 34 | items, _ = extract_items(response_json, "", ucid) 35 | return items.reject!(Category) 36 | end 37 | 38 | # Search inside of user subscriptions 39 | def subscriptions(query : Query, user : Invidious::User) : Array(ChannelVideo) 40 | view_name = "subscriptions_#{sha256(user.email)}" 41 | 42 | return PG_DB.query_all(" 43 | SELECT id,title,published,updated,ucid,author,length_seconds 44 | FROM ( 45 | SELECT *, 46 | to_tsvector(#{view_name}.title) || 47 | to_tsvector(#{view_name}.author) 48 | as document 49 | FROM #{view_name} 50 | ) v_search WHERE v_search.document @@ plainto_tsquery($1) LIMIT 20 OFFSET $2;", 51 | query.text, (query.page - 1) * 20, 52 | as: ChannelVideo 53 | ) 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /src/invidious/helpers/macros.cr: -------------------------------------------------------------------------------- 1 | module DB::Serializable 2 | macro included 3 | {% verbatim do %} 4 | macro finished 5 | def self.type_array 6 | \{{ @type.instance_vars 7 | .reject { |var| var.annotation(::DB::Field) && var.annotation(::DB::Field)[:ignore] } 8 | .map { |name| name.stringify } 9 | }} 10 | end 11 | 12 | def initialize(tuple) 13 | \{% for var in @type.instance_vars %} 14 | \{% ann = var.annotation(::DB::Field) %} 15 | \{% if ann && ann[:ignore] %} 16 | \{% else %} 17 | @\{{var.name}} = tuple[:\{{var.name.id}}] 18 | \{% end %} 19 | \{% end %} 20 | end 21 | 22 | def to_a 23 | \{{ @type.instance_vars 24 | .reject { |var| var.annotation(::DB::Field) && var.annotation(::DB::Field)[:ignore] } 25 | .map { |name| name } 26 | }} 27 | end 28 | end 29 | {% end %} 30 | end 31 | end 32 | 33 | module JSON::Serializable 34 | macro included 35 | {% verbatim do %} 36 | macro finished 37 | def initialize(tuple) 38 | \{% for var in @type.instance_vars %} 39 | \{% ann = var.annotation(::JSON::Field) %} 40 | \{% if ann && ann[:ignore] %} 41 | \{% else %} 42 | @\{{var.name}} = tuple[:\{{var.name.id}}] 43 | \{% end %} 44 | \{% end %} 45 | end 46 | end 47 | {% end %} 48 | end 49 | end 50 | 51 | macro templated(_filename, template = "template", navbar_search = true) 52 | navbar_search = {{navbar_search}} 53 | 54 | {{ filename = "src/invidious/views/" + _filename + ".ecr" }} 55 | {{ layout = "src/invidious/views/" + template + ".ecr" }} 56 | 57 | __content_filename__ = {{filename}} 58 | content = Kilt.render({{filename}}) 59 | Kilt.render({{layout}}) 60 | end 61 | 62 | macro rendered(filename) 63 | Kilt.render("src/invidious/views/#{{{filename}}}.ecr") 64 | end 65 | 66 | # Similar to Kemals halt method but works in a 67 | # method. 68 | macro haltf(env, status_code = 200, response = "") 69 | {{env}}.response.status_code = {{status_code}} 70 | {{env}}.response.print {{response}} 71 | {{env}}.response.close 72 | return 73 | end 74 | -------------------------------------------------------------------------------- /src/invidious/views/user/subscription_manager.ecr: -------------------------------------------------------------------------------- 1 | <% content_for "header" do %> 2 |<%= channel.description_html %>
28 |<%= env.get "access_token" %>
27 |