summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/ci.yml16
-rw-r--r--.github/workflows/container-release.yml20
-rw-r--r--.github/workflows/stale.yml4
-rw-r--r--README.md2
-rw-r--r--assets/js/_helpers.js249
-rw-r--r--assets/js/community.js86
-rw-r--r--assets/js/embed.js103
-rw-r--r--assets/js/handlers.js137
-rw-r--r--assets/js/notifications.js203
-rw-r--r--assets/js/player.js334
-rw-r--r--assets/js/playlist_widget.js52
-rw-r--r--assets/js/subscribe_widget.js92
-rw-r--r--assets/js/themes.js112
-rw-r--r--assets/js/watch.js475
-rw-r--r--assets/js/watched_widget.js39
-rw-r--r--scripts/deploy-database.sh60
-rw-r--r--scripts/install-dependencies.sh174
-rw-r--r--src/invidious/comments.cr2
-rw-r--r--src/invidious/views/licenses.ecr14
-rw-r--r--src/invidious/views/template.ecr1
-rw-r--r--src/invidious/views/watch.ecr38
21 files changed, 1144 insertions, 1069 deletions
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 4e68b7f2..6107e260 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -46,15 +46,15 @@ jobs:
stable: false
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3
- name: Install Crystal
- uses: crystal-lang/install-crystal@v1.5.3
+ uses: crystal-lang/install-crystal@v1.6.0
with:
crystal: ${{ matrix.crystal }}
- name: Cache Shards
- uses: actions/cache@v2
+ uses: actions/cache@v3
with:
path: ./lib
key: shards-${{ hashFiles('shard.lock') }}
@@ -84,7 +84,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3
- name: Build Docker
run: docker-compose build --build-arg release=0
@@ -100,18 +100,18 @@ jobs:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3
- name: Set up QEMU
- uses: docker/setup-qemu-action@v1
+ uses: docker/setup-qemu-action@v2
with:
platforms: arm64
- name: Set up Docker Buildx
- uses: docker/setup-buildx-action@v1
+ uses: docker/setup-buildx-action@v2
- name: Build Docker ARM64 image
- uses: docker/build-push-action@v2
+ uses: docker/build-push-action@v3
with:
context: .
file: docker/Dockerfile.arm64
diff --git a/.github/workflows/container-release.yml b/.github/workflows/container-release.yml
index 36fb566e..212487c8 100644
--- a/.github/workflows/container-release.yml
+++ b/.github/workflows/container-release.yml
@@ -15,20 +15,20 @@ on:
- screenshots/*
- .github/ISSUE_TEMPLATE/*
- kubernetes/**
-
+
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Checkout
- uses: actions/checkout@v2
-
+ uses: actions/checkout@v3
+
- name: Install Crystal
- uses: oprypin/install-crystal@v1.2.4
+ uses: crystal-lang/install-crystal@v1.6.0
with:
crystal: 1.2.2
-
+
- name: Run lint
run: |
if ! crystal tool format --check; then
@@ -38,15 +38,15 @@ jobs:
fi
- name: Set up QEMU
- uses: docker/setup-qemu-action@v1
+ uses: docker/setup-qemu-action@v2
with:
platforms: arm64
- name: Set up Docker Buildx
- uses: docker/setup-buildx-action@v1
+ uses: docker/setup-buildx-action@v2
- name: Login to registry
- uses: docker/login-action@v1
+ uses: docker/login-action@v2
with:
registry: quay.io
username: ${{ secrets.QUAY_USERNAME }}
@@ -54,7 +54,7 @@ jobs:
- name: Build and push Docker AMD64 image for Push Event
if: github.ref == 'refs/heads/master'
- uses: docker/build-push-action@v2
+ uses: docker/build-push-action@v3
with:
context: .
file: docker/Dockerfile
@@ -66,7 +66,7 @@ jobs:
- name: Build and push Docker ARM64 image for Push Event
if: github.ref == 'refs/heads/master'
- uses: docker/build-push-action@v2
+ uses: docker/build-push-action@v3
with:
context: .
file: docker/Dockerfile.arm64
diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml
index 86275da7..ff28d49b 100644
--- a/.github/workflows/stale.yml
+++ b/.github/workflows/stale.yml
@@ -10,11 +10,11 @@ jobs:
stale:
runs-on: ubuntu-latest
steps:
- - uses: actions/stale@v3
+ - uses: actions/stale@v5
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-stale: 365
- days-before-pr-stale: 45 # PRs should be active. Anything that hasn't had activity in more than 45 days should be considered abandoned.
+ days-before-pr-stale: 45 # PRs should be active. Anything that hasn't had activity in more than 45 days should be considered abandoned.
days-before-close: 30
exempt-pr-labels: blocked
stale-issue-message: 'This issue has been automatically marked as stale and will be closed in 30 days because it has not had recent activity and is much likely outdated. If you think this issue is still relevant and applicable, you just have to post a comment and it will be unmarked.'
diff --git a/README.md b/README.md
index d5369b5e..9ed68a4b 100644
--- a/README.md
+++ b/README.md
@@ -151,6 +151,8 @@ Weblate also allows you to log-in with major SSO providers like Github, Gitlab,
- [MusicPiped](https://github.com/deep-gaurav/MusicPiped): A material design music player that streams music from YouTube.
- [HoloPlay](https://github.com/stephane-r/HoloPlay): Funny Android application connecting on Invidious API's with search, playlists and favorites.
- [WatchTube](https://github.com/WatchTubeTeam/WatchTube): Powerful YouTube client for Apple Watch.
+- [Yattee](https://github.com/yattee/yattee): Alternative YouTube frontend for iPhone, iPad, Mac and Apple TV.
+- [TubiTui](https://codeberg.org/777/TubiTui): A lightweight, libre, TUI-based YouTube client.
## Liability
diff --git a/assets/js/_helpers.js b/assets/js/_helpers.js
new file mode 100644
index 00000000..7c50670e
--- /dev/null
+++ b/assets/js/_helpers.js
@@ -0,0 +1,249 @@
+'use strict';
+// Contains only auxiliary methods
+// May be included and executed unlimited number of times without any consequences
+
+// Polyfills for IE11
+Array.prototype.find = Array.prototype.find || function (condition) {
+ return this.filter(condition)[0];
+};
+Array.from = Array.from || function (source) {
+ return Array.prototype.slice.call(source);
+};
+NodeList.prototype.forEach = NodeList.prototype.forEach || function (callback) {
+ Array.from(this).forEach(callback);
+};
+String.prototype.includes = String.prototype.includes || function (searchString) {
+ return this.indexOf(searchString) >= 0;
+};
+String.prototype.startsWith = String.prototype.startsWith || function (prefix) {
+ return this.substr(0, prefix.length) === prefix;
+};
+Math.sign = Math.sign || function(x) {
+ x = +x;
+ if (!x) return x; // 0 and NaN
+ return x > 0 ? 1 : -1;
+};
+if (!window.hasOwnProperty('HTMLDetailsElement') && !window.hasOwnProperty('mockHTMLDetailsElement')) {
+ window.mockHTMLDetailsElement = true;
+ const style = 'details:not([open]) > :not(summary) {display: none}';
+ document.head.appendChild(document.createElement('style')).textContent = style;
+
+ addEventListener('click', function (e) {
+ if (e.target.nodeName !== 'SUMMARY') return;
+ const details = e.target.parentElement;
+ if (details.hasAttribute('open'))
+ details.removeAttribute('open');
+ else
+ details.setAttribute('open', '');
+ });
+}
+
+// Monstrous global variable for handy code
+// Includes: clamp, xhr, storage.{get,set,remove}
+window.helpers = window.helpers || {
+ /**
+ * https://en.wikipedia.org/wiki/Clamping_(graphics)
+ * @param {Number} num Source number
+ * @param {Number} min Low border
+ * @param {Number} max High border
+ * @returns {Number} Clamped value
+ */
+ clamp: function (num, min, max) {
+ if (max < min) {
+ var t = max; max = min; min = t; // swap max and min
+ }
+
+ if (max < num)
+ return max;
+ if (min > num)
+ return min;
+ return num;
+ },
+
+ /** @private */
+ _xhr: function (method, url, options, callbacks) {
+ const xhr = new XMLHttpRequest();
+ xhr.open(method, url);
+
+ // Default options
+ xhr.responseType = 'json';
+ xhr.timeout = 10000;
+ // Default options redefining
+ if (options.responseType)
+ xhr.responseType = options.responseType;
+ if (options.timeout)
+ xhr.timeout = options.timeout;
+
+ if (method === 'POST')
+ xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
+
+ // better than onreadystatechange because of 404 codes https://stackoverflow.com/a/36182963
+ xhr.onloadend = function () {
+ if (xhr.status === 200) {
+ if (callbacks.on200) {
+ // fix for IE11. It doesn't convert response to JSON
+ if (xhr.responseType === '' && typeof(xhr.response) === 'string')
+ callbacks.on200(JSON.parse(xhr.response));
+ else
+ callbacks.on200(xhr.response);
+ }
+ } else {
+ // handled by onerror
+ if (xhr.status === 0) return;
+
+ if (callbacks.onNon200)
+ callbacks.onNon200(xhr);
+ }
+ };
+
+ xhr.ontimeout = function () {
+ if (callbacks.onTimeout)
+ callbacks.onTimeout(xhr);
+ };
+
+ xhr.onerror = function () {
+ if (callbacks.onError)
+ callbacks.onError(xhr);
+ };
+
+ if (options.payload)
+ xhr.send(options.payload);
+ else
+ xhr.send();
+ },
+ /** @private */
+ _xhrRetry: function(method, url, options, callbacks) {
+ if (options.retries <= 0) {
+ console.warn('Failed to pull', options.entity_name);
+ if (callbacks.onTotalFail)
+ callbacks.onTotalFail();
+ return;
+ }
+ helpers._xhr(method, url, options, callbacks);
+ },
+ /**
+ * @callback callbackXhrOn200
+ * @param {Object} response - xhr.response
+ */
+ /**
+ * @callback callbackXhrError
+ * @param {XMLHttpRequest} xhr
+ */
+ /**
+ * @param {'GET'|'POST'} method - 'GET' or 'POST'
+ * @param {String} url - URL to send request to
+ * @param {Object} options - other XHR options
+ * @param {XMLHttpRequestBodyInit} [options.payload=null] - payload for POST-requests
+ * @param {'arraybuffer'|'blob'|'document'|'json'|'text'} [options.responseType=json]
+ * @param {Number} [options.timeout=10000]
+ * @param {Number} [options.retries=1]
+ * @param {String} [options.entity_name='unknown'] - string to log
+ * @param {Number} [options.retry_timeout=1000]
+ * @param {Object} callbacks - functions to execute on events fired
+ * @param {callbackXhrOn200} [callbacks.on200]
+ * @param {callbackXhrError} [callbacks.onNon200]
+ * @param {callbackXhrError} [callbacks.onTimeout]
+ * @param {callbackXhrError} [callbacks.onError]
+ * @param {callbackXhrError} [callbacks.onTotalFail] - if failed after all retries
+ */
+ xhr: function(method, url, options, callbacks) {
+ if (!options.retries || options.retries <= 1) {
+ helpers._xhr(method, url, options, callbacks);
+ return;
+ }
+
+ if (!options.entity_name) options.entity_name = 'unknown';
+ if (!options.retry_timeout) options.retry_timeout = 1000;
+ const retries_total = options.retries;
+ let currentTry = 1;
+
+ const retry = function () {
+ console.warn('Pulling ' + options.entity_name + ' failed... ' + (currentTry++) + '/' + retries_total);
+ setTimeout(function () {
+ options.retries--;
+ helpers._xhrRetry(method, url, options, callbacks);
+ }, options.retry_timeout);
+ };
+
+ // Pack retry() call into error handlers
+ callbacks._onError = callbacks.onError;
+ callbacks.onError = function (xhr) {
+ if (callbacks._onError)
+ callbacks._onError(xhr);
+ retry();
+ };
+ callbacks._onTimeout = callbacks.onTimeout;
+ callbacks.onTimeout = function (xhr) {
+ if (callbacks._onTimeout)
+ callbacks._onTimeout(xhr);
+ retry();
+ };
+
+ helpers._xhrRetry(method, url, options, callbacks);
+ },
+
+ /**
+ * @typedef {Object} invidiousStorage
+ * @property {(key:String) => Object} get
+ * @property {(key:String, value:Object)} set
+ * @property {(key:String)} remove
+ */
+
+ /**
+ * Universal storage, stores and returns JS objects. Uses inside localStorage or cookies
+ * @type {invidiousStorage}
+ */
+ storage: (function () {
+ // access to localStorage throws exception in Tor Browser, so try is needed
+ let localStorageIsUsable = false;
+ try{localStorageIsUsable = !!localStorage.setItem;}catch(e){}
+
+ if (localStorageIsUsable) {
+ return {
+ get: function (key) {
+ if (!localStorage[key]) return;
+ try {
+ return JSON.parse(decodeURIComponent(localStorage[key]));
+ } catch(e) {
+ // Erase non parsable value
+ helpers.storage.remove(key);
+ }
+ },
+ set: function (key, value) { localStorage[key] = encodeURIComponent(JSON.stringify(value)); },
+ remove: function (key) { localStorage.removeItem(key); }
+ };
+ }
+
+ // TODO: fire 'storage' event for cookies
+ console.info('Storage: localStorage is disabled or unaccessible. Cookies used as fallback');
+ return {
+ get: function (key) {
+ const cookiePrefix = key + '=';
+ function findCallback(cookie) {return cookie.startsWith(cookiePrefix);}
+ const matchedCookie = document.cookie.split('; ').find(findCallback);
+ if (matchedCookie) {
+ const cookieBody = matchedCookie.replace(cookiePrefix, '');
+ if (cookieBody.length === 0) return;
+ try {
+ return JSON.parse(decodeURIComponent(cookieBody));
+ } catch(e) {
+ // Erase non parsable value
+ helpers.storage.remove(key);
+ }
+ }
+ },
+ set: function (key, value) {
+ const cookie_data = encodeURIComponent(JSON.stringify(value));
+
+ // Set expiration in 2 year
+ const date = new Date();
+ date.setFullYear(date.getFullYear()+2);
+
+ document.cookie = key + '=' + cookie_data + '; expires=' + date.toGMTString();
+ },
+ remove: function (key) {
+ document.cookie = key + '=; Max-Age=0';
+ }
+ };
+ })()
+};
diff --git a/assets/js/community.js b/assets/js/community.js
index 44066a58..32fe4ebc 100644
--- a/assets/js/community.js
+++ b/assets/js/community.js
@@ -1,13 +1,6 @@
'use strict';
var community_data = JSON.parse(document.getElementById('community_data').textContent);
-String.prototype.supplant = function (o) {
- return this.replace(/{([^{}]*)}/g, function (a, b) {
- var r = o[b];
- return typeof r === 'string' || typeof r === 'number' ? r : a;
- });
-};
-
function hide_youtube_replies(event) {
var target = event.target;
@@ -38,13 +31,6 @@ function show_youtube_replies(event) {
target.setAttribute('data-sub-text', sub_text);
}
-function number_with_separator(val) {
- while (/(\d+)(\d{3})/.test(val.toString())) {
- val = val.toString().replace(/(\d+)(\d{3})/, '$1' + ',' + '$2');
- }
- return val;
-}
-
function get_youtube_replies(target, load_more) {
var continuation = target.getAttribute('data-continuation');
@@ -58,47 +44,39 @@ function get_youtube_replies(target, load_more) {
'&hl=' + community_data.preferences.locale +
'&thin_mode=' + community_data.preferences.thin_mode +
'&continuation=' + continuation;
- var xhr = new XMLHttpRequest();
- xhr.responseType = 'json';
- xhr.timeout = 10000;
- xhr.open('GET', url, true);
-
- xhr.onreadystatechange = function () {
- if (xhr.readyState === 4) {
- if (xhr.status === 200) {
- if (load_more) {
- body = body.parentNode.parentNode;
- body.removeChild(body.lastElementChild);
- body.innerHTML += xhr.response.contentHtml;
- } else {
- body.removeChild(body.lastElementChild);
-
- var p = document.createElement('p');
- var a = document.createElement('a');
- p.appendChild(a);
-
- a.href = 'javascript:void(0)';
- a.onclick = hide_youtube_replies;
- a.setAttribute('data-sub-text', community_data.hide_replies_text);
- a.setAttribute('data-inner-text', community_data.show_replies_text);
- a.innerText = community_data.hide_replies_text;
-
- var div = document.createElement('div');
- div.innerHTML = xhr.response.contentHtml;
-
- body.appendChild(p);
- body.appendChild(div);
- }
+
+ helpers.xhr('GET', url, {}, {
+ on200: function (response) {
+ if (load_more) {
+ body = body.parentNode.parentNode;
+ body.removeChild(body.lastElementChild);
+ body.innerHTML += response.contentHtml;
} else {
- body.innerHTML = fallback;
- }
- }
- };
+ body.removeChild(body.lastElementChild);
+
+ var p = document.createElement('p');
+ var a = document.createElement('a');
+ p.appendChild(a);
+
+ a.href = 'javascript:void(0)';
+ a.onclick = hide_youtube_replies;
+ a.setAttribute('data-sub-text', community_data.hide_replies_text);
+ a.setAttribute('data-inner-text', community_data.show_replies_text);
+ a.textContent = community_data.hide_replies_text;
- xhr.ontimeout = function () {
- console.warn('Pulling comments failed.');
- body.innerHTML = fallback;
- };
+ var div = document.createElement('div');
+ div.innerHTML = response.contentHtml;
- xhr.send();
+ body.appendChild(p);
+ body.appendChild(div);
+ }
+ },
+ onNon200: function (xhr) {
+ body.innerHTML = fallback;
+ },
+ onTimeout: function (xhr) {
+ console.warn('Pulling comments failed');
+ body.innerHTML = fallback;
+ }
+ });
}
diff --git a/assets/js/embed.js b/assets/js/embed.js
index 7e9ac605..b11b5e5a 100644
--- a/assets/js/embed.js
+++ b/assets/js/embed.js
@@ -1,14 +1,7 @@
'use strict';
var video_data = JSON.parse(document.getElementById('video_data').textContent);
-function get_playlist(plid, retries) {
- if (retries === undefined) retries = 5;
-
- if (retries <= 0) {
- console.warn('Failed to pull playlist');
- return;
- }
-
+function get_playlist(plid) {
var plid_url;
if (plid.startsWith('RD')) {
plid_url = '/api/v1/mixes/' + plid +
@@ -21,85 +14,49 @@ function get_playlist(plid, retries) {
'&format=html&hl=' + video_data.preferences.locale;
}
- var xhr = new XMLHttpRequest();
- xhr.responseType = 'json';
- xhr.timeout = 10000;
- xhr.open('GET', plid_url, true);
-
- xhr.onreadystatechange = function () {
- if (xhr.readyState === 4) {
- if (xhr.status === 200) {
- if (xhr.response.nextVideo) {
- player.on('ended', function () {
- var url = new URL('https://example.com/embed/' + xhr.response.nextVideo);
-
- url.searchParams.set('list', plid);
- if (!plid.startsWith('RD')) {
- url.searchParams.set('index', xhr.response.index);
- }
-
- if (video_data.params.autoplay || video_data.params.continue_autoplay) {
- url.searchParams.set('autoplay', '1');
- }
-
- if (video_data.params.listen !== video_data.preferences.listen) {
- url.searchParams.set('listen', video_data.params.listen);
- }
-
- if (video_data.params.speed !== video_data.preferences.speed) {
- url.searchParams.set('speed', video_data.params.speed);
- }
-
- if (video_data.params.local !== video_data.preferences.local) {
- url.searchParams.set('local', video_data.params.local);
- }
-
- location.assign(url.pathname + url.search);
- });
- }
- }
+ helpers.xhr('GET', plid_url, {retries: 5, entity_name: 'playlist'}, {
+ on200: function (response) {
+ if (!response.nextVideo)
+ return;
+
+ player.on('ended', function () {
+ var url = new URL('https://example.com/embed/' + response.nextVideo);
+
+ url.searchParams.set('list', plid);
+ if (!plid.startsWith('RD'))
+ url.searchParams.set('index', response.index);
+ if (video_data.params.autoplay || video_data.params.continue_autoplay)
+ url.searchParams.set('autoplay', '1');
+ if (video_data.params.listen !== video_data.preferences.listen)
+ url.searchParams.set('listen', video_data.params.listen);
+ if (video_data.params.speed !== video_data.preferences.speed)
+ url.searchParams.set('speed', video_data.params.speed);
+ if (video_data.params.local !== video_data.preferences.local)
+ url.searchParams.set('local', video_data.params.local);
+
+ location.assign(url.pathname + url.search);
+ });
}
- };
-
- xhr.onerror = function () {
- console.warn('Pulling playlist failed... ' + retries + '/5');
- setTimeout(function () { get_playlist(plid, retries - 1); }, 1000);
- };
-
- xhr.ontimeout = function () {
- console.warn('Pulling playlist failed... ' + retries + '/5');
- get_playlist(plid, retries - 1);
- };
-
- xhr.send();
+ });
}
-window.addEventListener('load', function (e) {
+addEventListener('load', function (e) {
if (video_data.plid) {
get_playlist(video_data.plid);
} else if (video_data.video_series) {
player.on('ended', function () {
var url = new URL('https://example.com/embed/' + video_data.video_series.shift());
- if (video_data.params.autoplay || video_data.params.continue_autoplay) {
+ if (video_data.params.autoplay || video_data.params.continue_autoplay)
url.searchParams.set('autoplay', '1');
- }
-
- if (video_data.params.listen !== video_data.preferences.listen) {
+ if (video_data.params.listen !== video_data.preferences.listen)
url.searchParams.set('listen', video_data.params.listen);
- }
-
- if (video_data.params.speed !== video_data.preferences.speed) {
+ if (video_data.params.speed !== video_data.preferences.speed)
url.searchParams.set('speed', video_data.params.speed);
- }
-
- if (video_data.params.local !== video_data.preferences.local) {
+ if (video_data.params.local !== video_data.preferences.local)
url.searchParams.set('local', video_data.params.local);
- }
-
- if (video_data.video_series.length !== 0) {
+ if (video_data.video_series.length !== 0)
url.searchParams.set('playlist', video_data.video_series.join(','));
- }
location.assign(url.pathname + url.search);
});
diff --git a/assets/js/handlers.js b/assets/js/handlers.js
index f6617b60..29810e72 100644
--- a/assets/js/handlers.js
+++ b/assets/js/handlers.js
@@ -1,8 +1,6 @@
'use strict';
(function () {
- var n2a = function (n) { return Array.prototype.slice.call(n); };
-
var video_player = document.getElementById('player_html5_api');
if (video_player) {
video_player.onmouseenter = function () { video_player['data-title'] = video_player['title']; video_player['title'] = ''; };
@@ -11,8 +9,8 @@
}
// For dynamically inserted elements
- document.addEventListener('click', function (e) {
- if (!e || !e.target) { return; }
+ addEventListener('click', function (e) {
+ if (!e || !e.target) return;
var t = e.target;
var handler_name = t.getAttribute('data-onclick');
@@ -29,6 +27,7 @@
get_youtube_replies(t, load_more, load_replies);
break;
case 'toggle_parent':
+ e.preventDefault();
toggle_parent(t);
break;
default:
@@ -36,118 +35,98 @@
}
});
- n2a(document.querySelectorAll('[data-mouse="switch_classes"]')).forEach(function (e) {
- var classes = e.getAttribute('data-switch-classes').split(',');
- var ec = classes[0];
- var lc = classes[1];
- var onoff = function (on, off) {
- var cs = e.getAttribute('class');
- cs = cs.split(off).join(on);
- e.setAttribute('class', cs);
- };
- e.onmouseenter = function () { onoff(ec, lc); };
- e.onmouseleave = function () { onoff(lc, ec); };
+ document.querySelectorAll('[data-mouse="switch_classes"]').forEach(function (el) {
+ var classes = el.getAttribute('data-switch-classes').split(',');
+ var classOnEnter = classes[0];
+ var classOnLeave = classes[1];
+ function toggle_classes(toAdd, toRemove) {
+ el.classList.add(toAdd);
+ el.classList.remove(toRemove);
+ }
+ el.onmouseenter = function () { toggle_classes(classOnEnter, classOnLeave); };
+ el.onmouseleave = function () { toggle_classes(classOnLeave, classOnEnter); };
});
- n2a(document.querySelectorAll('[data-onsubmit="return_false"]')).forEach(function (e) {
- e.onsubmit = function () { return false; };
+ document.querySelectorAll('[data-onsubmit="return_false"]').forEach(function (el) {
+ el.onsubmit = function () { return false; };
});
- n2a(document.querySelectorAll('[data-onclick="mark_watched"]')).forEach(function (e) {
- e.onclick = function () { mark_watched(e); };
+ document.querySelectorAll('[data-onclick="mark_watched"]').forEach(function (el) {
+ el.onclick = function () { mark_watched(el); };
});
- n2a(document.querySelectorAll('[data-onclick="mark_unwatched"]')).forEach(function (e) {
- e.onclick = function () { mark_unwatched(e); };
+ document.querySelectorAll('[data-onclick="mark_unwatched"]').forEach(function (el) {
+ el.onclick = function () { mark_unwatched(el); };
});
- n2a(document.querySelectorAll('[data-onclick="add_playlist_video"]')).forEach(function (e) {
- e.onclick = function () { add_playlist_video(e); };
+ document.querySelectorAll('[data-onclick="add_playlist_video"]').forEach(function (el) {
+ el.onclick = function () { add_playlist_video(el); };
});
- n2a(document.querySelectorAll('[data-onclick="add_playlist_item"]')).forEach(function (e) {
- e.onclick = function () { add_playlist_item(e); };
+ document.querySelectorAll('[data-onclick="add_playlist_item"]').forEach(function (el) {
+ el.onclick = function () { add_playlist_item(el); };
});
- n2a(document.querySelectorAll('[data-onclick="remove_playlist_item"]')).forEach(function (e) {
- e.onclick = function () { remove_playlist_item(e); };
+ document.querySelectorAll('[data-onclick="remove_playlist_item"]').forEach(function (el) {
+ el.onclick = function () { remove_playlist_item(el); };
});
- n2a(document.querySelectorAll('[data-onclick="revoke_token"]')).forEach(function (e) {
- e.onclick = function () { revoke_token(e); };
+ document.querySelectorAll('[data-onclick="revoke_token"]').forEach(function (el) {
+ el.onclick = function () { revoke_token(el); };
});
- n2a(document.querySelectorAll('[data-onclick="remove_subscription"]')).forEach(function (e) {
- e.onclick = function () { remove_subscription(e); };
+ document.querySelectorAll('[data-onclick="remove_subscription"]').forEach(function (el) {
+ el.onclick = function () { remove_subscription(el); };
});
- n2a(document.querySelectorAll('[data-onclick="notification_requestPermission"]')).forEach(function (e) {
- e.onclick = function () { Notification.requestPermission(); };
+ document.querySelectorAll('[data-onclick="notification_requestPermission"]').forEach(function (el) {
+ el.onclick = function () { Notification.requestPermission(); };
});
- n2a(document.querySelectorAll('[data-onrange="update_volume_value"]')).forEach(function (e) {
- var cb = function () { update_volume_value(e); };
- e.oninput = cb;
- e.onchange = cb;
+ document.querySelectorAll('[data-onrange="update_volume_value"]').forEach(function (el) {
+ function update_volume_value() {
+ document.getElementById('volume-value').textContent = el.value;
+ }
+ el.oninput = update_volume_value;
+ el.onchange = update_volume_value;
});
- function update_volume_value(element) {
- document.getElementById('volume-value').innerText = element.value;
- }
function revoke_token(target) {
var row = target.parentNode.parentNode.parentNode.parentNode.parentNode;
row.style.display = 'none';
var count = document.getElementById('count');
- count.innerText = count.innerText - 1;
+ count.textContent--;
- var referer = window.encodeURIComponent(document.location.href);
var url = '/token_ajax?action_revoke_token=1&redirect=false' +
- '&referer=' + referer +
+ '&referer=' + encodeURIComponent(location.href) +
'&session=' + target.getAttribute('data-session');
- var xhr = new XMLHttpRequest();
- xhr.responseType = 'json';
- xhr.timeout = 10000;
- xhr.open('POST', url, true);
- xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
-
- xhr.onreadystatechange = function () {
- if (xhr.readyState === 4) {
- if (xhr.status !== 200) {
- count.innerText = parseInt(count.innerText) + 1;
- row.style.display = '';
- }
- }
- };
- var csrf_token = target.parentNode.querySelector('input[name="csrf_token"]').value;
- xhr.send('csrf_token=' + csrf_token);
+ var payload = 'csrf_token=' + target.parentNode.querySelector('input[name="csrf_token"]').value;
+
+ helpers.xhr('POST', url, {payload: payload}, {
+ onNon200: function (xhr) {
+ count.textContent++;
+ row.style.display = '';
+ }
+ });
}
function remove_subscription(target) {
var row = target.parentNode.parentNode.parentNode.parentNode.parentNode;
row.style.display = 'none';
var count = document.getElementById('count');
- count.innerText = count.innerText - 1;
+ count.textContent--;
- var referer = window.encodeURIComponent(document.location.href);
var url = '/subscription_ajax?action_remove_subscriptions=1&redirect=false' +
- '&referer=' + referer +
+ '&referer=' + encodeURIComponent(location.href) +
'&c=' + target.getAttribute('data-ucid');
- var xhr = new XMLHttpRequest();
- xhr.responseType = 'json';
- xhr.timeout = 10000;
- xhr.open('POST', url, true);
- xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
-
- xhr.onreadystatechange = function () {
- if (xhr.readyState === 4) {
- if (xhr.status !== 200) {
- count.innerText = parseInt(count.innerText) + 1;
- row.style.display = '';
- }
- }
- };
- var csrf_token = target.parentNode.querySelector('input[name="csrf_token"]').value;
- xhr.send('csrf_token=' + csrf_token);
+ var payload = 'csrf_token=' + target.parentNode.querySelector('input[name="csrf_token"]').value;
+
+ helpers.xhr('POST', url, {payload: payload}, {
+ onNon200: function (xhr) {
+ count.textContent++;
+ row.style.display = '';
+ }
+ });
}
// Handle keypresses
- window.addEventListener('keydown', function (event) {
+ addEventListener('keydown', function (event) {
// Ignore modifier keys
if (event.ctrlKey || event.metaKey) return;
diff --git a/assets/js/notifications.js b/assets/js/notifications.js
index ec5f6dd3..058553d9 100644
--- a/assets/js/notifications.js
+++ b/assets/js/notifications.js
@@ -1,43 +1,26 @@
'use strict';
var notification_data = JSON.parse(document.getElementById('notification_data').textContent);
-var notifications, delivered;
-
-function get_subscriptions(callback, retries) {
- if (retries === undefined) retries = 5;
-
- if (retries <= 0) {
- return;
- }
-
- var xhr = new XMLHttpRequest();
- xhr.responseType = 'json';
- xhr.timeout = 10000;
- xhr.open('GET', '/api/v1/auth/subscriptions?fields=authorId', true);
-
- xhr.onreadystatechange = function () {
- if (xhr.readyState === 4) {
- if (xhr.status === 200) {
- var subscriptions = xhr.response;
- callback(subscriptions);
- }
- }
- };
-
- xhr.onerror = function () {
- console.warn('Pulling subscriptions failed... ' + retries + '/5');
- setTimeout(function () { get_subscriptions(callback, retries - 1); }, 1000);
- };
+/** Boolean meaning 'some tab have stream' */
+const STORAGE_KEY_STREAM = 'stream';
+/** Number of notifications. May be increased or reset */
+const STORAGE_KEY_NOTIF_COUNT = 'notification_count';
- xhr.ontimeout = function () {
- console.warn('Pulling subscriptions failed... ' + retries + '/5');
- get_subscriptions(callback, retries - 1);
- };
-
- xhr.send();
+var notifications, delivered;
+var notifications_mock = { close: function () { } };
+
+function get_subscriptions() {
+ helpers.xhr('GET', '/api/v1/auth/subscriptions?fields=authorId', {
+ retries: 5,
+ entity_name: 'subscriptions'
+ }, {
+ on200: create_notification_stream
+ });
}
function create_notification_stream(subscriptions) {
+ // sse.js can't be replaced to EventSource in place as it lack support of payload and headers
+ // see https://developer.mozilla.org/en-US/docs/Web/API/EventSource/EventSource
notifications = new SSE(
'/api/v1/auth/notifications?fields=videoId,title,author,authorId,publishedText,published,authorThumbnails,liveNow', {
withCredentials: true,
@@ -49,96 +32,100 @@ function create_notification_stream(subscriptions) {
var start_time = Math.round(new Date() / 1000);
notifications.onmessage = function (event) {
- if (!event.id) {
- return;
- }
+ if (!event.id) return;
var notification = JSON.parse(event.data);
console.info('Got notification:', notification);
- if (start_time < notification.published && !delivered.includes(notification.videoId)) {
- if (Notification.permission === 'granted') {
- var system_notification =
- new Notification((notification.liveNow ? notification_data.live_now_text : notification_data.upload_text).replace('`x`', notification.author), {
- body: notification.title,
- icon: '/ggpht' + new URL(notification.authorThumbnails[2].url).pathname,
- img: '/ggpht' + new URL(notification.authorThumbnails[4].url).pathname,
- tag: notification.videoId
- });
-
- system_notification.onclick = function (event) {
- window.open('/watch?v=' + event.currentTarget.tag, '_blank');
- };
- }
-
- delivered.push(notification.videoId);
- localStorage.setItem('notification_count', parseInt(localStorage.getItem('notification_count') || '0') + 1);
- var notification_ticker = document.getElementById('notification_ticker');
-
- if (parseInt(localStorage.getItem('notification_count')) > 0) {
- notification_ticker.innerHTML =
- '<span id="notification_count">' + localStorage.getItem('notification_count') + '</span> <i class="icon ion-ios-notifications"></i>';
- } else {
- notification_ticker.innerHTML =
- '<i class="icon ion-ios-notifications-outline"></i>';
- }
+ // Ignore not actual and delivered notifications
+ if (start_time > notification.published || delivered.includes(notification.videoId)) return;
+
+ delivered.push(notification.videoId);
+
+ let notification_count = helpers.storage.get(STORAGE_KEY_NOTIF_COUNT) || 0;
+ notification_count++;
+ helpers.storage.set(STORAGE_KEY_NOTIF_COUNT, notification_count);
+
+ update_ticker_count();
+
+ // permission for notifications handled on settings page. JS handler is in handlers.js
+ if (window.Notification && Notification.permission === 'granted') {
+ var notification_text = notification.liveNow ? notification_data.live_now_text : notification_data.upload_text;
+ notification_text = notification_text.replace('`x`', notification.author);
+
+ var system_notification = new Notification(notification_text, {
+ body: notification.title,
+ icon: '/ggpht' + new URL(notification.authorThumbnails[2].url).pathname,
+ img: '/ggpht' + new URL(notification.authorThumbnails[4].url).pathname
+ });
+
+ system_notification.onclick = function (e) {
+ open('/watch?v=' + notification.videoId, '_blank');
+ };
}
};
- notifications.addEventListener('error', handle_notification_error);
- notifications.stream();
-}
+ notifications.addEventListener('error', function (e) {
+ console.warn('Something went wrong with notifications, trying to reconnect...');
+ notifications = notifications_mock;
+ setTimeout(get_subscriptions, 1000);
+ });
-function handle_notification_error(event) {
- console.warn('Something went wrong with notifications, trying to reconnect...');
- notifications = { close: function () { } };
- setTimeout(function () { get_subscriptions(create_notification_stream); }, 1000);
+ notifications.stream();
}
-window.addEventListener('load', function (e) {
- localStorage.setItem('notification_count', document.getElementById('notification_count') ? document.getElementById('notification_count').innerText : '0');
+function update_ticker_count() {
+ var notification_ticker = document.getElementById('notification_ticker');
- if (localStorage.getItem('stream')) {
- localStorage.removeItem('stream');
+ const notification_count = helpers.storage.get(STORAGE_KEY_STREAM);
+ if (notification_count > 0) {
+ notification_ticker.innerHTML =
+ '<span id="notification_count">' + notification_count + '</span> <i class="icon ion-ios-notifications"></i>';
} else {
- setTimeout(function () {
- if (!localStorage.getItem('stream')) {
- notifications = { close: function () { } };
- localStorage.setItem('stream', true);
- get_subscriptions(create_notification_stream);
- }
- }, Math.random() * 1000 + 50);
+ notification_ticker.innerHTML =
+ '<i class="icon ion-ios-notifications-outline"></i>';
}
+}
- window.addEventListener('storage', function (e) {
- if (e.key === 'stream' && !e.newValue) {
- if (notifications) {
- localStorage.setItem('stream', true);
- } else {
- setTimeout(function () {
- if (!localStorage.getItem('stream')) {
- notifications = { close: function () { } };
- localStorage.setItem('stream', true);
- get_subscriptions(create_notification_stream);
- }
- }, Math.random() * 1000 + 50);
- }
- } else if (e.key === 'notification_count') {
- var notification_ticker = document.getElementById('notification_ticker');
-
- if (parseInt(e.newValue) > 0) {
- notification_ticker.innerHTML =
- '<span id="notification_count">' + e.newValue + '</span> <i class="icon ion-ios-notifications"></i>';
- } else {
- notification_ticker.innerHTML =
- '<i class="icon ion-ios-notifications-outline"></i>';
- }
+function start_stream_if_needed() {
+ // random wait for other tabs set 'stream' flag
+ setTimeout(function () {
+ if (!helpers.storage.get(STORAGE_KEY_STREAM)) {
+ // if no one set 'stream', set it by yourself and start stream
+ helpers.storage.set(STORAGE_KEY_STREAM, true);
+ notifications = notifications_mock;
+ get_subscriptions();
}
- });
-});
+ }, Math.random() * 1000 + 50); // [0.050 .. 1.050) second
+}
+
+
+addEventListener('storage', function (e) {
+ if (e.key === STORAGE_KEY_NOTIF_COUNT)
+ update_ticker_count();
-window.addEventListener('unload', function (e) {
- if (notifications) {
- localStorage.removeItem('stream');
+ // if 'stream' key was removed
+ if (e.key === STORAGE_KEY_STREAM && !helpers.storage.get(STORAGE_KEY_STREAM)) {
+ if (notifications) {
+ // restore it if we have active stream
+ helpers.storage.set(STORAGE_KEY_STREAM, true);
+ } else {
+ start_stream_if_needed();
+ }
}
});
+
+addEventListener('load', function () {
+ var notification_count_el = document.getElementById('notification_count');
+ var notification_count = notification_count_el ? parseInt(notification_count_el.textContent) : 0;
+ helpers.storage.set(STORAGE_KEY_NOTIF_COUNT, notification_count);
+
+ if (helpers.storage.get(STORAGE_KEY_STREAM))
+ helpers.storage.remove(STORAGE_KEY_STREAM);
+ start_stream_if_needed();
+});
+
+addEventListener('unload', function () {
+ // let chance to other tabs to be a streamer via firing 'storage' event
+ if (notifications) helpers.storage.remove(STORAGE_KEY_STREAM);
+});
diff --git a/assets/js/player.js b/assets/js/player.js
index 6ddb1158..7d099e66 100644
--- a/assets/js/player.js
+++ b/assets/js/player.js
@@ -42,45 +42,53 @@ embed_url = location.origin + '/embed/' + video_data.id + embed_url.search;
var save_player_pos_key = 'save_player_pos';
videojs.Vhs.xhr.beforeRequest = function(options) {
- if (options.uri.indexOf('videoplayback') === -1 && options.uri.indexOf('local=true') === -1) {
- options.uri = options.uri + '?local=true';
+ // set local if requested not videoplayback
+ if (!options.uri.includes('videoplayback')) {
+ if (!options.uri.includes('local=true'))
+ options.uri += '?local=true';
}
return options;
};
var player = videojs('player', options);
-player.on('error', () => {
- if (video_data.params.quality !== 'dash') {
- if (!player.currentSrc().includes("local=true") && !video_data.local_disabled) {
- var currentSources = player.currentSources();
- for (var i = 0; i < currentSources.length; i++) {
- currentSources[i]["src"] += "&local=true"
- }
- player.src(currentSources)
- }
- else if (player.error().code === 2 || player.error().code === 4) {
- setTimeout(function (event) {
- console.log('An error occurred in the player, reloading...');
-
- var currentTime = player.currentTime();
- var playbackRate = player.playbackRate();
- var paused = player.paused();
-
- player.load();
-
- if (currentTime > 0.5) currentTime -= 0.5;
-
- player.currentTime(currentTime);
- player.playbackRate(playbackRate);
-
- if (!paused) player.play();
- }, 10000);
- }
+player.on('error', function () {
+ if (video_data.params.quality === 'dash') return;
+
+ var localNotDisabled = (
+ !player.currentSrc().includes('local=true') && !video_data.local_disabled
+ );
+ var reloadMakesSense = (
+ player.error().code === MediaError.MEDIA_ERR_NETWORK ||
+ player.error().code === MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED
+ );
+
+ if (localNotDisabled) {
+ // add local=true to all current sources
+ player.src(player.currentSources().map(function (source) {
+ source.src += '&local=true';
+ }));
+ } else if (reloadMakesSense) {
+ setTimeout(function () {
+ console.warn('An error occurred in the player, reloading...');
+
+ // After load() all parameters are reset. Save them
+ var currentTime = player.currentTime();
+ var playbackRate = player.playbackRate();
+ var paused = player.paused();
+
+ player.load();
+
+ if (currentTime > 0.5) currentTime -= 0.5;
+
+ player.currentTime(currentTime);
+ player.playbackRate(playbackRate);
+ if (!paused) player.play();
+ }, 5000);
}
});
-if (video_data.params.quality == 'dash') {
+if (video_data.params.quality === 'dash') {
player.reloadSourceOnError({
errorInterval: 10
});
@@ -89,7 +97,7 @@ if (video_data.params.quality == 'dash') {
/**
* Function for add time argument to url
* @param {String} url
- * @returns urlWithTimeArg
+ * @returns {URL} urlWithTimeArg
*/
function addCurrentTimeToURL(url) {
var urlUsed = new URL(url);
@@ -112,18 +120,12 @@ var shareOptions = {
description: player_data.description,
image: player_data.thumbnail,
get embedCode() {
- return '<iframe id="ivplayer" width="640" height="360" src="' +
- addCurrentTimeToURL(embed_url) + '" style="border:none;"></iframe>';
+ // Single quotes inside here required. HTML inserted as is into value attribute of input
+ return "<iframe id='ivplayer' width='640' height='360' src='" +
+ addCurrentTimeToURL(embed_url) + "' style='border:none;'></iframe>";
}
};
-const storage = (function () {
- try { if (localStorage.length !== -1) return localStorage; }
- catch (e) { console.info('No storage available: ' + e); }
-
- return undefined;
-})();
-
if (location.pathname.startsWith('/embed/')) {
var overlay_content = '<h1><a rel="noopener" target="_blank" href="' + location.origin + '/watch?v=' + video_data.id + '">' + player_data.title + '</a></h1>';
player.overlay({
@@ -162,7 +164,7 @@ if (isMobile()) {
buttons.forEach(function (child) {primary_control_bar.removeChild(child);});
var operations_bar_element = operations_bar.el();
- operations_bar_element.className += ' mobile-operations-bar';
+ operations_bar_element.classList.add('mobile-operations-bar');
player.addChild(operations_bar);
// Playback menu doesn't work when it's initialized outside of the primary control bar
@@ -175,8 +177,8 @@ if (isMobile()) {
operations_bar_element.append(share_element);
if (video_data.params.quality === 'dash') {
- var http_source_selector = document.getElementsByClassName('vjs-http-source-selector vjs-menu-button')[0];
- operations_bar_element.append(http_source_selector);
+ var http_source_selector = document.getElementsByClassName('vjs-http-source-selector vjs-menu-button')[0];
+ operations_bar_element.append(http_source_selector);
}
});
}
@@ -220,14 +222,14 @@ player.playbackRate(video_data.params.speed);
* Method for getting the contents of a cookie
*
* @param {String} name Name of cookie
- * @returns cookieValue
+ * @returns {String|null} cookieValue
*/
function getCookieValue(name) {
- var value = document.cookie.split(';').filter(function (item) {return item.includes(name + '=');});
-
- return (value.length >= 1)
- ? value[0].substring((name + '=').length, value[0].length)
- : null;
+ var cookiePrefix = name + '=';
+ var matchedCookie = document.cookie.split(';').find(function (item) {return item.includes(cookiePrefix);});
+ if (matchedCookie)
+ return matchedCookie.replace(cookiePrefix, '');
+ return null;
}
/**
@@ -257,11 +259,11 @@ function updateCookie(newVolume, newSpeed) {
date.setTime(date.getTime() + 63115200);
var ipRegex = /^((\d+\.){3}\d+|[A-Fa-f0-9]*:[A-Fa-f0-9:]*:[A-Fa-f0-9:]+)$/;
- var domainUsed = window.location.hostname;
+ var domainUsed = location.hostname;
// Fix for a bug in FF where the leading dot in the FQDN is not ignored
if (domainUsed.charAt(0) !== '.' && !ipRegex.test(domainUsed) && domainUsed !== 'localhost')
- domainUsed = '.' + window.location.hostname;
+ domainUsed = '.' + location.hostname;
document.cookie = 'PREFS=' + cookieData + '; SameSite=Strict; path=/; domain=' +
domainUsed + '; expires=' + date.toGMTString() + ';';
@@ -280,7 +282,7 @@ player.on('volumechange', function () {
player.on('waiting', function () {
if (player.playbackRate() > 1 && player.liveTracker.isLive() && player.liveTracker.atLiveEdge()) {
- console.info('Player has caught up to source, resetting playbackRate.');
+ console.info('Player has caught up to source, resetting playbackRate');
player.playbackRate(1);
}
});
@@ -292,12 +294,12 @@ if (video_data.premiere_timestamp && Math.round(new Date() / 1000) < video_data.
if (video_data.params.save_player_pos) {
const url = new URL(location);
const hasTimeParam = url.searchParams.has('t');
- const remeberedTime = get_video_time();
+ const rememberedTime = get_video_time();
let lastUpdated = 0;
- if(!hasTimeParam) set_seconds_after_start(remeberedTime);
+ if(!hasTimeParam) set_seconds_after_start(rememberedTime);
- const updateTime = function () {
+ player.on('timeupdate', function () {
const raw = player.currentTime();
const time = Math.floor(raw);
@@ -305,9 +307,7 @@ if (video_data.params.save_player_pos) {
save_video_time(time);
lastUpdated = time;
}
- };
-
- player.on('timeupdate', updateTime);
+ });
}
else remove_all_video_times();
@@ -347,53 +347,31 @@ if (!video_data.params.listen && video_data.params.quality === 'dash') {
targetQualityLevel = 0;
break;
default:
- const targetHeight = Number.parseInt(video_data.params.quality_dash, 10);
+ const targetHeight = parseInt(video_data.params.quality_dash);
for (let i = 0; i < qualityLevels.length; i++) {
- if (qualityLevels[i].height <= targetHeight) {
+ if (qualityLevels[i].height <= targetHeight)
targetQualityLevel = i;
- } else {
+ else
break;
- }
}
}
- for (let i = 0; i < qualityLevels.length; i++) {
- qualityLevels[i].enabled = (i === targetQualityLevel);
- }
+ qualityLevels.forEach(function (level, index) {
+ level.enabled = (index === targetQualityLevel);
+ });
});
});
}
}
player.vttThumbnails({
- src: location.origin + '/api/v1/storyboards/' + video_data.id + '?height=90',
+ src: '/api/v1/storyboards/' + video_data.id + '?height=90',
showTimestamp: true
});
// Enable annotations
if (!video_data.params.listen && video_data.params.annotations) {
- window.addEventListener('load', function (e) {
- var video_container = document.getElementById('player');
- let xhr = new XMLHttpRequest();
- xhr.responseType = 'text';
- xhr.timeout = 60000;
- xhr.open('GET', '/api/v1/annotations/' + video_data.id, true);
-
- xhr.onreadystatechange = function () {
- if (xhr.readyState === 4) {
- if (xhr.status === 200) {
- videojs.registerPlugin('youtubeAnnotationsPlugin', youtubeAnnotationsPlugin);
- if (!player.paused()) {
- player.youtubeAnnotationsPlugin({ annotationXml: xhr.response, videoContainer: video_container });
- } else {
- player.one('play', function (event) {
- player.youtubeAnnotationsPlugin({ annotationXml: xhr.response, videoContainer: video_container });
- });
- }
- }
- }
- };
-
- window.addEventListener('__ar_annotation_click', function (e) {
+ addEventListener('load', function (e) {
+ addEventListener('__ar_annotation_click', function (e) {
const url = e.detail.url,
target = e.detail.target,
seconds = e.detail.seconds;
@@ -406,41 +384,48 @@ if (!video_data.params.listen && video_data.params.annotations) {
path = path.pathname + path.search;
if (target === 'current') {
- window.location.href = path;
+ location.href = path;
} else if (target === 'new') {
- window.open(path, '_blank');
+ open(path, '_blank');
+ }
+ });
+
+ helpers.xhr('GET', '/api/v1/annotations/' + video_data.id, {
+ responseType: 'text',
+ timeout: 60000
+ }, {
+ on200: function (response) {
+ var video_container = document.getElementById('player');
+ videojs.registerPlugin('youtubeAnnotationsPlugin', youtubeAnnotationsPlugin);
+ if (player.paused()) {
+ player.one('play', function (event) {
+ player.youtubeAnnotationsPlugin({ annotationXml: response, videoContainer: video_container });
+ });
+ } else {
+ player.youtubeAnnotationsPlugin({ annotationXml: response, videoContainer: video_container });
+ }
}
});
- xhr.send();
});
}
-function increase_volume(delta) {
+function change_volume(delta) {
const curVolume = player.volume();
let newVolume = curVolume + delta;
- if (newVolume > 1) {
- newVolume = 1;
- } else if (newVolume < 0) {
- newVolume = 0;
- }
+ newVolume = helpers.clamp(newVolume, 0, 1);
player.volume(newVolume);
}
function toggle_muted() {
- const isMuted = player.muted();
- player.muted(!isMuted);
+ player.muted(!player.muted());
}
function skip_seconds(delta) {
const duration = player.duration();
const curTime = player.currentTime();
let newTime = curTime + delta;
- if (newTime > duration) {
- newTime = duration;
- } else if (newTime < 0) {
- newTime = 0;
- }
+ newTime = helpers.clamp(newTime, 0, duration);
player.currentTime(newTime);
}
@@ -450,57 +435,21 @@ function set_seconds_after_start(delta) {
}
function save_video_time(seconds) {
- const videoId = video_data.id;
const all_video_times = get_all_video_times();
-
- all_video_times[videoId] = seconds;
-
- set_all_video_times(all_video_times);
+ all_video_times[video_data.id] = seconds;
+ helpers.storage.set(save_player_pos_key, all_video_times);
}
function get_video_time() {
- try {
- const videoId = video_data.id;
- const all_video_times = get_all_video_times();
- const timestamp = all_video_times[videoId];
-
- return timestamp || 0;
- }
- catch (e) {
- return 0;
- }
-}
-
-function set_all_video_times(times) {
- if (storage) {
- if (times) {
- try {
- storage.setItem(save_player_pos_key, JSON.stringify(times));
- } catch (e) {
- console.warn('set_all_video_times: ' + e);
- }
- } else {
- storage.removeItem(save_player_pos_key);
- }
- }
+ return get_all_video_times()[video_data.id] || 0;
}
function get_all_video_times() {
- if (storage) {
- const raw = storage.getItem(save_player_pos_key);
- if (raw !== null) {
- try {
- return JSON.parse(raw);
- } catch (e) {
- console.warn('get_all_video_times: ' + e);
- }
- }
- }
- return {};
+ return helpers.storage.get(save_player_pos_key) || {};
}
function remove_all_video_times() {
- set_all_video_times(null);
+ helpers.storage.remove(save_player_pos_key);
}
function set_time_percent(percent) {
@@ -516,21 +465,23 @@ function toggle_play() { player.paused() ? play() : pause(); }
const toggle_captions = (function () {
let toggledTrack = null;
- const onChange = function (e) {
- toggledTrack = null;
- };
- const bindChange = function (onOrOff) {
- player.textTracks()[onOrOff]('change', onChange);
- };
+
+ function bindChange(onOrOff) {
+ player.textTracks()[onOrOff]('change', function (e) {
+ toggledTrack = null;
+ });
+ }
+
// Wrapper function to ignore our own emitted events and only listen
// to events emitted by Video.js on click on the captions menu items.
- const setMode = function (track, mode) {
+ function setMode(track, mode) {
bindChange('off');
track.mode = mode;
- window.setTimeout(function () {
+ setTimeout(function () {
bindChange('on');
}, 0);
- };
+ }
+
bindChange('on');
return function () {
if (toggledTrack !== null) {
@@ -578,15 +529,11 @@ function increase_playback_rate(steps) {
const maxIndex = options.playbackRates.length - 1;
const curIndex = options.playbackRates.indexOf(player.playbackRate());
let newIndex = curIndex + steps;
- if (newIndex > maxIndex) {
- newIndex = maxIndex;
- } else if (newIndex < 0) {
- newIndex = 0;
- }
+ newIndex = helpers.clamp(newIndex, 0, maxIndex);
player.playbackRate(options.playbackRates[newIndex]);
}
-window.addEventListener('keydown', function (e) {
+addEventListener('keydown', function (e) {
if (e.target.tagName.toLowerCase() === 'input') {
// Ignore input when focus is on certain elements, e.g. form fields.
return;
@@ -619,10 +566,10 @@ window.addEventListener('keydown', function (e) {
case 'MediaStop': action = stop; break;
case 'ArrowUp':
- if (isPlayerFocused) action = increase_volume.bind(this, 0.1);
+ if (isPlayerFocused) action = change_volume.bind(this, 0.1);
break;
case 'ArrowDown':
- if (isPlayerFocused) action = increase_volume.bind(this, -0.1);
+ if (isPlayerFocused) action = change_volume.bind(this, -0.1);
break;
case 'm':
@@ -673,12 +620,11 @@ window.addEventListener('keydown', function (e) {
// TODO: Add support to play back previous video.
break;
- case '.':
- // TODO: Add support for next-frame-stepping.
- break;
- case ',':
- // TODO: Add support for previous-frame-stepping.
- break;
+ // TODO: More precise step. Now FPS is taken equal to 29.97
+ // Common FPS: https://forum.videohelp.com/threads/81868#post323588
+ // Possible solution is new HTMLVideoElement.requestVideoFrameCallback() https://wicg.github.io/video-rvfc/
+ case ',': action = function () { pause(); skip_seconds(-1/29.97); }; break;
+ case '.': action = function () { pause(); skip_seconds( 1/29.97); }; break;
case '>': action = increase_playback_rate.bind(this, 1); break;
case '<': action = increase_playback_rate.bind(this, -1); break;
@@ -697,10 +643,6 @@ window.addEventListener('keydown', function (e) {
// Add support for controlling the player volume by scrolling over it. Adapted from
// https://github.com/ctd1500/videojs-hotkeys/blob/bb4a158b2e214ccab87c2e7b95f42bc45c6bfd87/videojs.hotkeys.js#L292-L328
(function () {
- const volumeStep = 0.05;
- const enableVolumeScroll = true;
- const enableHoverScroll = true;
- const doc = document;
const pEl = document.getElementById('player');
var volumeHover = false;
@@ -710,39 +652,23 @@ window.addEventListener('keydown', function (e) {
volumeSelector.onmouseout = function () { volumeHover = false; };
}
- var mouseScroll = function mouseScroll(event) {
- var activeEl = doc.activeElement;
- if (enableHoverScroll) {
- // If we leave this undefined then it can match non-existent elements below
- activeEl = 0;
- }
-
+ function mouseScroll(event) {
// When controls are disabled, hotkeys will be disabled as well
- if (player.controls()) {
- if (volumeHover) {
- if (enableVolumeScroll) {
- event = window.event || event;
- var delta = Math.max(-1, Math.min(1, (event.wheelDelta || -event.detail)));
- event.preventDefault();
-
- if (delta === 1) {
- increase_volume(volumeStep);
- } else if (delta === -1) {
- increase_volume(-volumeStep);
- }
- }
- }
- }
- };
+ if (!player.controls() || !volumeHover) return;
+
+ event.preventDefault();
+ var wheelMove = event.wheelDelta || -event.detail;
+ var volumeSign = Math.sign(wheelMove);
+
+ change_volume(volumeSign * 0.05); // decrease/increase by 5%
+ }
player.on('mousewheel', mouseScroll);
player.on('DOMMouseScroll', mouseScroll);
}());
// Since videojs-share can sometimes be blocked, we defer it until last
-if (player.share) {
- player.share(shareOptions);
-}
+if (player.share) player.share(shareOptions);
// show the preferred caption by default
if (player_data.preferred_caption_found) {
@@ -763,7 +689,7 @@ if (navigator.vendor === 'Apple Computer, Inc.' && video_data.params.listen) {
}
// Watch on Invidious link
-if (window.location.pathname.startsWith('/embed/')) {
+if (location.pathname.startsWith('/embed/')) {
const Button = videojs.getComponent('Button');
let watch_on_invidious_button = new Button(player);
@@ -778,3 +704,11 @@ if (window.location.pathname.startsWith('/embed/')) {
var cb = player.getChild('ControlBar');
cb.addChild(watch_on_invidious_button);
}
+
+addEventListener('DOMContentLoaded', function () {
+ // Save time during redirection on another instance
+ const changeInstanceLink = document.querySelector('#watch-on-another-invidious-instance > a');
+ if (changeInstanceLink) changeInstanceLink.addEventListener('click', function () {
+ changeInstanceLink.href = addCurrentTimeToURL(changeInstanceLink.href);
+ });
+});
diff --git a/assets/js/playlist_widget.js b/assets/js/playlist_widget.js
index c2565874..c92592ac 100644
--- a/assets/js/playlist_widget.js
+++ b/assets/js/playlist_widget.js
@@ -1,5 +1,6 @@
'use strict';
var playlist_data = JSON.parse(document.getElementById('playlist_data').textContent);
+var payload = 'csrf_token=' + playlist_data.csrf_token;
function add_playlist_video(target) {
var select = target.parentNode.children[0].children[1];
@@ -8,21 +9,12 @@ function add_playlist_video(target) {
var url = '/playlist_ajax?action_add_video=1&redirect=false' +
'&video_id=' + target.getAttribute('data-id') +
'&playlist_id=' + option.getAttribute('data-plid');
- var xhr = new XMLHttpRequest();
- xhr.responseType = 'json';
- xhr.timeout = 10000;
- xhr.open('POST', url, true);
- xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
- xhr.onreadystatechange = function () {
- if (xhr.readyState === 4) {
- if (xhr.status === 200) {
- option.innerText = '✓' + option.innerText;
- }
+ helpers.xhr('POST', url, {payload: payload}, {
+ on200: function (response) {
+ option.textContent = '✓' + option.textContent;
}
- };
-
- xhr.send('csrf_token=' + playlist_data.csrf_token);
+ });
}
function add_playlist_item(target) {
@@ -32,21 +24,12 @@ function add_playlist_item(target) {
var url = '/playlist_ajax?action_add_video=1&redirect=false' +
'&video_id=' + target.getAttribute('data-id') +
'&playlist_id=' + target.getAttribute('data-plid');
- var xhr = new XMLHttpRequest();
- xhr.responseType = 'json';
- xhr.timeout = 10000;
- xhr.open('POST', url, true);
- xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
- xhr.onreadystatechange = function () {
- if (xhr.readyState === 4) {
- if (xhr.status !== 200) {
- tile.style.display = '';
- }
+ helpers.xhr('POST', url, {payload: payload}, {
+ onNon200: function (xhr) {
+ tile.style.display = '';
}
- };
-
- xhr.send('csrf_token=' + playlist_data.csrf_token);
+ });
}
function remove_playlist_item(target) {
@@ -56,19 +39,10 @@ function remove_playlist_item(target) {
var url = '/playlist_ajax?action_remove_video=1&redirect=false' +
'&set_video_id=' + target.getAttribute('data-index') +
'&playlist_id=' + target.getAttribute('data-plid');
- var xhr = new XMLHttpRequest();
- xhr.responseType = 'json';
- xhr.timeout = 10000;
- xhr.open('POST', url, true);
- xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
- xhr.onreadystatechange = function () {
- if (xhr.readyState === 4) {
- if (xhr.status !== 200) {
- tile.style.display = '';
- }
+ helpers.xhr('POST', url, {payload: payload}, {
+ onNon200: function (xhr) {
+ tile.style.display = '';
}
- };
-
- xhr.send('csrf_token=' + playlist_data.csrf_token);
+ });
}
diff --git a/assets/js/subscribe_widget.js b/assets/js/subscribe_widget.js
index 45ff5706..7665a00b 100644
--- a/assets/js/subscribe_widget.js
+++ b/assets/js/subscribe_widget.js
@@ -1,8 +1,9 @@
'use strict';
var subscribe_data = JSON.parse(document.getElementById('subscribe_data').textContent);
+var payload = 'csrf_token=' + subscribe_data.csrf_token;
var subscribe_button = document.getElementById('subscribe');
-subscribe_button.parentNode['action'] = 'javascript:void(0)';
+subscribe_button.parentNode.action = 'javascript:void(0)';
if (subscribe_button.getAttribute('data-type') === 'subscribe') {
subscribe_button.onclick = subscribe;
@@ -10,87 +11,34 @@ if (subscribe_button.getAttribute('data-type') === 'subscribe') {
subscribe_button.onclick = unsubscribe;
}
-function subscribe(retries) {
- if (retries === undefined) retries = 5;
-
- if (retries <= 0) {
- console.warn('Failed to subscribe.');
- return;
- }
-
- var url = '/subscription_ajax?action_create_subscription_to_channel=1&redirect=false' +
- '&c=' + subscribe_data.ucid;
- var xhr = new XMLHttpRequest();
- xhr.responseType = 'json';
- xhr.timeout = 10000;
- xhr.open('POST', url, true);
- xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
-
+function subscribe() {
var fallback = subscribe_button.innerHTML;
subscribe_button.onclick = unsubscribe;
subscribe_button.innerHTML = '<b>' + subscribe_data.unsubscribe_text + ' | ' + subscribe_data.sub_count_text + '</b>';
- xhr.onreadystatechange = function () {
- if (xhr.readyState === 4) {
- if (xhr.status !== 200) {
- subscribe_button.onclick = subscribe;
- subscribe_button.innerHTML = fallback;
- }
- }
- };
-
- xhr.onerror = function () {
- console.warn('Subscribing failed... ' + retries + '/5');
- setTimeout(function () { subscribe(retries - 1); }, 1000);
- };
-
- xhr.ontimeout = function () {
- console.warn('Subscribing failed... ' + retries + '/5');
- subscribe(retries - 1);
- };
+ var url = '/subscription_ajax?action_create_subscription_to_channel=1&redirect=false' +
+ '&c=' + subscribe_data.ucid;
- xhr.send('csrf_token=' + subscribe_data.csrf_token);
+ helpers.xhr('POST', url, {payload: payload, retries: 5, entity_name: 'subscribe request'}, {
+ onNon200: function (xhr) {
+ subscribe_button.onclick = subscribe;
+ subscribe_button.innerHTML = fallback;
+ }
+ });
}
-function unsubscribe(retries) {
- if (retries === undefined)
- retries = 5;
-
- if (retries <= 0) {
- console.warn('Failed to subscribe');
- return;
- }
-
- var url = '/subscription_ajax?action_remove_subscriptions=1&redirect=false' +
- '&c=' + subscribe_data.ucid;
- var xhr = new XMLHttpRequest();
- xhr.responseType = 'json';
- xhr.timeout = 10000;
- xhr.open('POST', url, true);
- xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
-
+function unsubscribe() {
var fallback = subscribe_button.innerHTML;
subscribe_button.onclick = subscribe;
subscribe_button.innerHTML = '<b>' + subscribe_data.subscribe_text + ' | ' + subscribe_data.sub_count_text + '</b>';
- xhr.onreadystatechange = function () {
- if (xhr.readyState === 4) {
- if (xhr.status !== 200) {
- subscribe_button.onclick = unsubscribe;
- subscribe_button.innerHTML = fallback;
- }
- }
- };
-
- xhr.onerror = function () {
- console.warn('Unsubscribing failed... ' + retries + '/5');
- setTimeout(function () { unsubscribe(retries - 1); }, 1000);
- };
-
- xhr.ontimeout = function () {
- console.warn('Unsubscribing failed... ' + retries + '/5');
- unsubscribe(retries - 1);
- };
+ var url = '/subscription_ajax?action_remove_subscriptions=1&redirect=false' +
+ '&c=' + subscribe_data.ucid;
- xhr.send('csrf_token=' + subscribe_data.csrf_token);
+ helpers.xhr('POST', url, {payload: payload, retries: 5, entity_name: 'unsubscribe request'}, {
+ onNon200: function (xhr) {
+ subscribe_button.onclick = unsubscribe;
+ subscribe_button.innerHTML = fallback;
+ }
+ });
}
diff --git a/assets/js/themes.js b/assets/js/themes.js
index 3f503b38..76767d5f 100644
--- a/assets/js/themes.js
+++ b/assets/js/themes.js
@@ -1,90 +1,44 @@
'use strict';
var toggle_theme = document.getElementById('toggle_theme');
-toggle_theme.href = 'javascript:void(0);';
+toggle_theme.href = 'javascript:void(0)';
-toggle_theme.addEventListener('click', function () {
- var dark_mode = document.body.classList.contains('light-theme');
-
- var url = '/toggle_theme?redirect=false';
- var xhr = new XMLHttpRequest();
- xhr.responseType = 'json';
- xhr.timeout = 10000;
- xhr.open('GET', url, true);
-
- set_mode(dark_mode);
- try {
- window.localStorage.setItem('dark_mode', dark_mode ? 'dark' : 'light');
- } catch (e) {}
+const STORAGE_KEY_THEME = 'dark_mode';
+const THEME_DARK = 'dark';
+const THEME_LIGHT = 'light';
- xhr.send();
-});
-
-window.addEventListener('storage', function (e) {
- if (e.key === 'dark_mode') {
- update_mode(e.newValue);
- }
-});
-
-window.addEventListener('DOMContentLoaded', function () {
- const dark_mode = document.getElementById('dark_mode_pref').textContent;
- try {
- // Update localStorage if dark mode preference changed on preferences page
- window.localStorage.setItem('dark_mode', dark_mode);
- } catch (e) {}
- update_mode(dark_mode);
+// TODO: theme state controlled by system
+toggle_theme.addEventListener('click', function () {
+ const isDarkTheme = helpers.storage.get(STORAGE_KEY_THEME) === THEME_DARK;
+ const newTheme = isDarkTheme ? THEME_LIGHT : THEME_DARK;
+ setTheme(newTheme);
+ helpers.storage.set(STORAGE_KEY_THEME, newTheme);
+ helpers.xhr('GET', '/toggle_theme?redirect=false', {}, {});
});
-
-var darkScheme = window.matchMedia('(prefers-color-scheme: dark)');
-var lightScheme = window.matchMedia('(prefers-color-scheme: light)');
-
-darkScheme.addListener(scheme_switch);
-lightScheme.addListener(scheme_switch);
-
-function scheme_switch (e) {
- // ignore this method if we have a preference set
- try {
- if (localStorage.getItem('dark_mode')) {
- return;
- }
- } catch (exception) {}
- if (e.matches) {
- if (e.media.includes('dark')) {
- set_mode(true);
- } else if (e.media.includes('light')) {
- set_mode(false);
- }
- }
-}
-
-function set_mode (bool) {
- if (bool) {
- // dark
- toggle_theme.children[0].setAttribute('class', 'icon ion-ios-sunny');
- document.body.classList.remove('no-theme');
- document.body.classList.remove('light-theme');
- document.body.classList.add('dark-theme');
+/** @param {THEME_DARK|THEME_LIGHT} theme */
+function setTheme(theme) {
+ // By default body element has .no-theme class that uses OS theme via CSS @media rules
+ // It rewrites using hard className below
+ if (theme === THEME_DARK) {
+ toggle_theme.children[0].className = 'icon ion-ios-sunny';
+ document.body.className = 'dark-theme';
} else {
- // light
- toggle_theme.children[0].setAttribute('class', 'icon ion-ios-moon');
- document.body.classList.remove('no-theme');
- document.body.classList.remove('dark-theme');
- document.body.classList.add('light-theme');
+ toggle_theme.children[0].className = 'icon ion-ios-moon';
+ document.body.className = 'light-theme';
}
}
-function update_mode (mode) {
- if (mode === 'true' /* for backwards compatibility */ || mode === 'dark') {
- // If preference for dark mode indicated
- set_mode(true);
- }
- else if (mode === 'false' /* for backwards compatibility */ || mode === 'light') {
- // If preference for light mode indicated
- set_mode(false);
- }
- else if (document.getElementById('dark_mode_pref').textContent === '' && window.matchMedia('(prefers-color-scheme: dark)').matches) {
- // If no preference indicated here and no preference indicated on the preferences page (backend), but the browser tells us that the operating system has a dark theme
- set_mode(true);
+// Handles theme change event caused by other tab
+addEventListener('storage', function (e) {
+ if (e.key === STORAGE_KEY_THEME)
+ setTheme(helpers.storage.get(STORAGE_KEY_THEME));
+});
+
+// Set theme from preferences on page load
+addEventListener('DOMContentLoaded', function () {
+ const prefTheme = document.getElementById('dark_mode_pref').textContent;
+ if (prefTheme) {
+ setTheme(prefTheme);
+ helpers.storage.set(STORAGE_KEY_THEME, prefTheme);
}
- // else do nothing, falling back to the mode defined by the `dark_mode` preference on the preferences page (backend)
-}
+});
diff --git a/assets/js/watch.js b/assets/js/watch.js
index 29d58be5..cff84e4d 100644
--- a/assets/js/watch.js
+++ b/assets/js/watch.js
@@ -1,5 +1,7 @@
'use strict';
var video_data = JSON.parse(document.getElementById('video_data').textContent);
+var spinnerHTML = '<h3 style="text-align:center"><div class="loading"><i class="icon ion-ios-refresh"></i></div></h3>';
+var spinnerHTMLwithHR = spinnerHTML + '<hr>';
String.prototype.supplant = function (o) {
return this.replace(/{([^{}]*)}/g, function (a, b) {
@@ -10,24 +12,24 @@ String.prototype.supplant = function (o) {
function toggle_parent(target) {
var body = target.parentNode.parentNode.children[1];
- if (body.style.display === null || body.style.display === '') {
- target.textContent = '[ + ]';
- body.style.display = 'none';
- } else {
+ if (body.style.display === 'none') {
target.textContent = '[ − ]';
body.style.display = '';
+ } else {
+ target.textContent = '[ + ]';
+ body.style.display = 'none';
}
}
function toggle_comments(event) {
var target = event.target;
var body = target.parentNode.parentNode.parentNode.children[1];
- if (body.style.display === null || body.style.display === '') {
- target.textContent = '[ + ]';
- body.style.display = 'none';
- } else {
+ if (body.style.display === 'none') {
target.textContent = '[ − ]';
body.style.display = '';
+ } else {
+ target.textContent = '[ + ]';
+ body.style.display = 'none';
}
}
@@ -79,56 +81,31 @@ if (continue_button) {
function next_video() {
var url = new URL('https://example.com/watch?v=' + video_data.next_video);
- if (video_data.params.autoplay || video_data.params.continue_autoplay) {
+ if (video_data.params.autoplay || video_data.params.continue_autoplay)
url.searchParams.set('autoplay', '1');
- }
-
- if (video_data.params.listen !== video_data.preferences.listen) {
+ if (video_data.params.listen !== video_data.preferences.listen)
url.searchParams.set('listen', video_data.params.listen);
- }
-
- if (video_data.params.speed !== video_data.preferences.speed) {
+ if (video_data.params.speed !== video_data.preferences.speed)
url.searchParams.set('speed', video_data.params.speed);
- }
-
- if (video_data.params.local !== video_data.preferences.local) {
+ if (video_data.params.local !== video_data.preferences.local)
url.searchParams.set('local', video_data.params.local);
- }
-
url.searchParams.set('continue', '1');
+
location.assign(url.pathname + url.search);
}
function continue_autoplay(event) {
if (event.target.checked) {
- player.on('ended', function () {
- next_video();
- });
+ player.on('ended', next_video);
} else {
player.off('ended');
}
}
-function number_with_separator(val) {
- while (/(\d+)(\d{3})/.test(val.toString())) {
- val = val.toString().replace(/(\d+)(\d{3})/, '$1' + ',' + '$2');
- }
- return val;
-}
-
-function get_playlist(plid, retries) {
- if (retries === undefined) retries = 5;
+function get_playlist(plid) {
var playlist = document.getElementById('playlist');
- if (retries <= 0) {
- console.warn('Failed to pull playlist');
- playlist.innerHTML = '';
- return;
- }
-
- playlist.innerHTML = ' \
- <h3 style="text-align:center"><div class="loading"><i class="icon ion-ios-refresh"></i></div></h3> \
- <hr>';
+ playlist.innerHTML = spinnerHTMLwithHR;
var plid_url;
if (plid.startsWith('RD')) {
@@ -142,225 +119,148 @@ function get_playlist(plid, retries) {
'&format=html&hl=' + video_data.preferences.locale;
}
- var xhr = new XMLHttpRequest();
- xhr.responseType = 'json';
- xhr.timeout = 10000;
- xhr.open('GET', plid_url, true);
-
- xhr.onreadystatechange = function () {
- if (xhr.readyState === 4) {
- if (xhr.status === 200) {
- playlist.innerHTML = xhr.response.playlistHtml;
- var nextVideo = document.getElementById(xhr.response.nextVideo);
- nextVideo.parentNode.parentNode.scrollTop = nextVideo.offsetTop;
-
- if (xhr.response.nextVideo) {
- player.on('ended', function () {
- var url = new URL('https://example.com/watch?v=' + xhr.response.nextVideo);
-
- url.searchParams.set('list', plid);
- if (!plid.startsWith('RD')) {
- url.searchParams.set('index', xhr.response.index);
- }
-
- if (video_data.params.autoplay || video_data.params.continue_autoplay) {
- url.searchParams.set('autoplay', '1');
- }
-
- if (video_data.params.listen !== video_data.preferences.listen) {
- url.searchParams.set('listen', video_data.params.listen);
- }
-
- if (video_data.params.speed !== video_data.preferences.speed) {
- url.searchParams.set('speed', video_data.params.speed);
- }
-
- if (video_data.params.local !== video_data.preferences.local) {
- url.searchParams.set('local', video_data.params.local);
- }
-
- location.assign(url.pathname + url.search);
- });
- }
- } else {
- playlist.innerHTML = '';
- document.getElementById('continue').style.display = '';
- }
+ helpers.xhr('GET', plid_url, {retries: 5, entity_name: 'playlist'}, {
+ on200: function (response) {
+ playlist.innerHTML = response.playlistHtml;
+
+ if (!response.nextVideo) return;
+
+ var nextVideo = document.getElementById(response.nextVideo);
+ nextVideo.parentNode.parentNode.scrollTop = nextVideo.offsetTop;
+
+ player.on('ended', function () {
+ var url = new URL('https://example.com/watch?v=' + response.nextVideo);
+
+ url.searchParams.set('list', plid);
+ if (!plid.startsWith('RD'))
+ url.searchParams.set('index', response.index);
+ if (video_data.params.autoplay || video_data.params.continue_autoplay)
+ url.searchParams.set('autoplay', '1');
+ if (video_data.params.listen !== video_data.preferences.listen)
+ url.searchParams.set('listen', video_data.params.listen);
+ if (video_data.params.speed !== video_data.preferences.speed)
+ url.searchParams.set('speed', video_data.params.speed);
+ if (video_data.params.local !== video_data.preferences.local)
+ url.searchParams.set('local', video_data.params.local);
+
+ location.assign(url.pathname + url.search);
+ });
+ },
+ onNon200: function (xhr) {
+ playlist.innerHTML = '';
+ document.getElementById('continue').style.display = '';
+ },
+ onError: function (xhr) {
+ playlist.innerHTML = spinnerHTMLwithHR;
+ },
+ onTimeout: function (xhr) {
+ playlist.innerHTML = spinnerHTMLwithHR;
}
- };
-
- xhr.onerror = function () {
- playlist = document.getElementById('playlist');
- playlist.innerHTML =
- '<h3 style="text-align:center"><div class="loading"><i class="icon ion-ios-refresh"></i></div></h3><hr>';
-
- console.warn('Pulling playlist timed out... ' + retries + '/5');
- setTimeout(function () { get_playlist(plid, retries - 1); }, 1000);
- };
-
- xhr.ontimeout = function () {
- playlist = document.getElementById('playlist');
- playlist.innerHTML =
- '<h3 style="text-align:center"><div class="loading"><i class="icon ion-ios-refresh"></i></div></h3><hr>';
-
- console.warn('Pulling playlist timed out... ' + retries + '/5');
- get_playlist(plid, retries - 1);
- };
-
- xhr.send();
+ });
}
-function get_reddit_comments(retries) {
- if (retries === undefined) retries = 5;
+function get_reddit_comments() {
var comments = document.getElementById('comments');
- if (retries <= 0) {
- console.warn('Failed to pull comments');
- comments.innerHTML = '';
- return;
- }
-
var fallback = comments.innerHTML;
- comments.innerHTML =
- '<h3 style="text-align:center"><div class="loading"><i class="icon ion-ios-refresh"></i></div></h3>';
+ comments.innerHTML = spinnerHTML;
var url = '/api/v1/comments/' + video_data.id +
'?source=reddit&format=html' +
'&hl=' + video_data.preferences.locale;
- var xhr = new XMLHttpRequest();
- xhr.responseType = 'json';
- xhr.timeout = 10000;
- xhr.open('GET', url, true);
-
- xhr.onreadystatechange = function () {
- if (xhr.readyState === 4) {
- if (xhr.status === 200) {
- comments.innerHTML = ' \
- <div> \
- <h3> \
- <a href="javascript:void(0)">[ − ]</a> \
- {title} \
- </h3> \
- <p> \
- <b> \
- <a href="javascript:void(0)" data-comments="youtube"> \
- {youtubeCommentsText} \
- </a> \
- </b> \
- </p> \
+
+ var onNon200 = function (xhr) { comments.innerHTML = fallback; };
+ if (video_data.params.comments[1] === 'youtube')
+ onNon200 = function (xhr) {};
+
+ helpers.xhr('GET', url, {retries: 5, entity_name: ''}, {
+ on200: function (response) {
+ comments.innerHTML = ' \
+ <div> \
+ <h3> \
+ <a href="javascript:void(0)">[ − ]</a> \
+ {title} \
+ </h3> \
+ <p> \
<b> \
- <a rel="noopener" target="_blank" href="https://reddit.com{permalink}">{redditPermalinkText}</a> \
+ <a href="javascript:void(0)" data-comments="youtube"> \
+ {youtubeCommentsText} \
+ </a> \
</b> \
- </div> \
- <div>{contentHtml}</div> \
- <hr>'.supplant({
- title: xhr.response.title,
- youtubeCommentsText: video_data.youtube_comments_text,
- redditPermalinkText: video_data.reddit_permalink_text,
- permalink: xhr.response.permalink,
- contentHtml: xhr.response.contentHtml
- });
-
- comments.children[0].children[0].children[0].onclick = toggle_comments;
- comments.children[0].children[1].children[0].onclick = swap_comments;
- } else {
- if (video_data.params.comments[1] === 'youtube') {
- console.warn('Pulling comments failed... ' + retries + '/5');
- setTimeout(function () { get_youtube_comments(retries - 1); }, 1000);
- } else {
- comments.innerHTML = fallback;
- }
- }
- }
- };
-
- xhr.onerror = function () {
- console.warn('Pulling comments failed... ' + retries + '/5');
- setTimeout(function () { get_reddit_comments(retries - 1); }, 1000);
- };
-
- xhr.ontimeout = function () {
- console.warn('Pulling comments failed... ' + retries + '/5');
- get_reddit_comments(retries - 1);
- };
-
- xhr.send();
+ </p> \
+ <b> \
+ <a rel="noopener" target="_blank" href="https://reddit.com{permalink}">{redditPermalinkText}</a> \
+ </b> \
+ </div> \
+ <div>{contentHtml}</div> \
+ <hr>'.supplant({
+ title: response.title,
+ youtubeCommentsText: video_data.youtube_comments_text,
+ redditPermalinkText: video_data.reddit_permalink_text,
+ permalink: response.permalink,
+ contentHtml: response.contentHtml
+ });
+
+ comments.children[0].children[0].children[0].onclick = toggle_comments;
+ comments.children[0].children[1].children[0].onclick = swap_comments;
+ },
+ onNon200: onNon200, // declared above
+ });
}
-function get_youtube_comments(retries) {
- if (retries === undefined) retries = 5;
+function get_youtube_comments() {
var comments = document.getElementById('comments');
- if (retries <= 0) {
- console.warn('Failed to pull comments');
- comments.innerHTML = '';
- return;
- }
-
var fallback = comments.innerHTML;
- comments.innerHTML =
- '<h3 style="text-align:center"><div class="loading"><i class="icon ion-ios-refresh"></i></div></h3>';
+ comments.innerHTML = spinnerHTML;
var url = '/api/v1/comments/' + video_data.id +
'?format=html' +
'&hl=' + video_data.preferences.locale +
'&thin_mode=' + video_data.preferences.thin_mode;
- var xhr = new XMLHttpRequest();
- xhr.responseType = 'json';
- xhr.timeout = 10000;
- xhr.open('GET', url, true);
-
- xhr.onreadystatechange = function () {
- if (xhr.readyState === 4) {
- if (xhr.status === 200) {
- comments.innerHTML = ' \
- <div> \
- <h3> \
- <a href="javascript:void(0)">[ − ]</a> \
- {commentsText} \
- </h3> \
- <b> \
- <a href="javascript:void(0)" data-comments="reddit"> \
- {redditComments} \
- </a> \
- </b> \
- </div> \
- <div>{contentHtml}</div> \
- <hr>'.supplant({
- contentHtml: xhr.response.contentHtml,
- redditComments: video_data.reddit_comments_text,
- commentsText: video_data.comments_text.supplant(
- { commentCount: number_with_separator(xhr.response.commentCount) }
- )
- });
-
- comments.children[0].children[0].children[0].onclick = toggle_comments;
- comments.children[0].children[1].children[0].onclick = swap_comments;
- } else {
- if (video_data.params.comments[1] === 'youtube') {
- setTimeout(function () { get_youtube_comments(retries - 1); }, 1000);
- } else {
- comments.innerHTML = '';
- }
- }
+
+ var onNon200 = function (xhr) { comments.innerHTML = fallback; };
+ if (video_data.params.comments[1] === 'youtube')
+ onNon200 = function (xhr) {};
+
+ helpers.xhr('GET', url, {retries: 5, entity_name: 'comments'}, {
+ on200: function (response) {
+ comments.innerHTML = ' \
+ <div> \
+ <h3> \
+ <a href="javascript:void(0)">[ − ]</a> \
+ {commentsText} \
+ </h3> \
+ <b> \
+ <a href="javascript:void(0)" data-comments="reddit"> \
+ {redditComments} \
+ </a> \
+ </b> \
+ </div> \
+ <div>{contentHtml}</div> \
+ <hr>'.supplant({
+ contentHtml: response.contentHtml,
+ redditComments: video_data.reddit_comments_text,
+ commentsText: video_data.comments_text.supplant({
+ // toLocaleString correctly splits number with local thousands separator. e.g.:
+ // '1,234,567.89' for user with English locale
+ // '1 234 567,89' for user with Russian locale
+ // '1.234.567,89' for user with Portuguese locale
+ commentCount: response.commentCount.toLocaleString()
+ })
+ });
+
+ comments.children[0].children[0].children[0].onclick = toggle_comments;
+ comments.children[0].children[1].children[0].onclick = swap_comments;
+ },
+ onNon200: onNon200, // declared above
+ onError: function (xhr) {
+ comments.innerHTML = spinnerHTML;
+ },
+ onTimeout: function (xhr) {
+ comments.innerHTML = spinnerHTML;
}
- };
-
- xhr.onerror = function () {
- comments.innerHTML =
- '<h3 style="text-align:center"><div class="loading"><i class="icon ion-ios-refresh"></i></div></h3>';
- console.warn('Pulling comments failed... ' + retries + '/5');
- setTimeout(function () { get_youtube_comments(retries - 1); }, 1000);
- };
-
- xhr.ontimeout = function () {
- comments.innerHTML =
- '<h3 style="text-align:center"><div class="loading"><i class="icon ion-ios-refresh"></i></div></h3>';
- console.warn('Pulling comments failed... ' + retries + '/5');
- get_youtube_comments(retries - 1);
- };
-
- xhr.send();
+ });
}
function get_youtube_replies(target, load_more, load_replies) {
@@ -368,91 +268,72 @@ function get_youtube_replies(target, load_more, load_replies) {
var body = target.parentNode.parentNode;
var fallback = body.innerHTML;
- body.innerHTML =
- '<h3 style="text-align:center"><div class="loading"><i class="icon ion-ios-refresh"></i></div></h3>';
+ body.innerHTML = spinnerHTML;
var url = '/api/v1/comments/' + video_data.id +
'?format=html' +
'&hl=' + video_data.preferences.locale +
'&thin_mode=' + video_data.preferences.thin_mode +
'&continuation=' + continuation;
- if (load_replies) {
- url += '&action=action_get_comment_replies';
- }
- var xhr = new XMLHttpRequest();
- xhr.responseType = 'json';
- xhr.timeout = 10000;
- xhr.open('GET', url, true);
-
- xhr.onreadystatechange = function () {
- if (xhr.readyState === 4) {
- if (xhr.status === 200) {
- if (load_more) {
- body = body.parentNode.parentNode;
- body.removeChild(body.lastElementChild);
- body.innerHTML += xhr.response.contentHtml;
- } else {
- body.removeChild(body.lastElementChild);
-
- var p = document.createElement('p');
- var a = document.createElement('a');
- p.appendChild(a);
-
- a.href = 'javascript:void(0)';
- a.onclick = hide_youtube_replies;
- a.setAttribute('data-sub-text', video_data.hide_replies_text);
- a.setAttribute('data-inner-text', video_data.show_replies_text);
- a.innerText = video_data.hide_replies_text;
-
- var div = document.createElement('div');
- div.innerHTML = xhr.response.contentHtml;
-
- body.appendChild(p);
- body.appendChild(div);
- }
+ if (load_replies) url += '&action=action_get_comment_replies';
+
+ helpers.xhr('GET', url, {}, {
+ on200: function (response) {
+ if (load_more) {
+ body = body.parentNode.parentNode;
+ body.removeChild(body.lastElementChild);
+ body.innerHTML += response.contentHtml;
} else {
- body.innerHTML = fallback;
- }
- }
- };
+ body.removeChild(body.lastElementChild);
+
+ var p = document.createElement('p');
+ var a = document.createElement('a');
+ p.appendChild(a);
+
+ a.href = 'javascript:void(0)';
+ a.onclick = hide_youtube_replies;
+ a.setAttribute('data-sub-text', video_data.hide_replies_text);
+ a.setAttribute('data-inner-text', video_data.show_replies_text);
+ a.textContent = video_data.hide_replies_text;
- xhr.ontimeout = function () {
- console.warn('Pulling comments failed.');
- body.innerHTML = fallback;
- };
+ var div = document.createElement('div');
+ div.innerHTML = response.contentHtml;
- xhr.send();
+ body.appendChild(p);
+ body.appendChild(div);
+ }
+ },
+ onNon200: function (xhr) {
+ body.innerHTML = fallback;
+ },
+ onTimeout: function (xhr) {
+ console.warn('Pulling comments failed');
+ body.innerHTML = fallback;
+ }
+ });
}
if (video_data.play_next) {
player.on('ended', function () {
var url = new URL('https://example.com/watch?v=' + video_data.next_video);
- if (video_data.params.autoplay || video_data.params.continue_autoplay) {
+ if (video_data.params.autoplay || video_data.params.continue_autoplay)
url.searchParams.set('autoplay', '1');
- }
-
- if (video_data.params.listen !== video_data.preferences.listen) {
+ if (video_data.params.listen !== video_data.preferences.listen)
url.searchParams.set('listen', video_data.params.listen);
- }
-
- if (video_data.params.speed !== video_data.preferences.speed) {
+ if (video_data.params.speed !== video_data.preferences.speed)
url.searchParams.set('speed', video_data.params.speed);
- }
-
- if (video_data.params.local !== video_data.preferences.local) {
+ if (video_data.params.local !== video_data.preferences.local)
url.searchParams.set('local', video_data.params.local);
- }
-
url.searchParams.set('continue', '1');
+
location.assign(url.pathname + url.search);
});
}
-window.addEventListener('load', function (e) {
- if (video_data.plid) {
+addEventListener('load', function (e) {
+ if (video_data.plid)
get_playlist(video_data.plid);
- }
if (video_data.params.comments[0] === 'youtube') {
get_youtube_comments();
diff --git a/assets/js/watched_widget.js b/assets/js/watched_widget.js
index 87989a79..f1ac9cb4 100644
--- a/assets/js/watched_widget.js
+++ b/assets/js/watched_widget.js
@@ -1,5 +1,6 @@
'use strict';
var watched_data = JSON.parse(document.getElementById('watched_data').textContent);
+var payload = 'csrf_token=' + watched_data.csrf_token;
function mark_watched(target) {
var tile = target.parentNode.parentNode.parentNode.parentNode.parentNode;
@@ -7,45 +8,27 @@ function mark_watched(target) {
var url = '/watch_ajax?action_mark_watched=1&redirect=false' +
'&id=' + target.getAttribute('data-id');
- var xhr = new XMLHttpRequest();
- xhr.responseType = 'json';
- xhr.timeout = 10000;
- xhr.open('POST', url, true);
- xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
- xhr.onreadystatechange = function () {
- if (xhr.readyState === 4) {
- if (xhr.status !== 200) {
- tile.style.display = '';
- }
+ helpers.xhr('POST', url, {payload: payload}, {
+ onNon200: function (xhr) {
+ tile.style.display = '';
}
- };
-
- xhr.send('csrf_token=' + watched_data.csrf_token);
+ });
}
function mark_unwatched(target) {
var tile = target.parentNode.parentNode.parentNode.parentNode.parentNode;
tile.style.display = 'none';
var count = document.getElementById('count');
- count.innerText = count.innerText - 1;
+ count.textContent--;
var url = '/watch_ajax?action_mark_unwatched=1&redirect=false' +
'&id=' + target.getAttribute('data-id');
- var xhr = new XMLHttpRequest();
- xhr.responseType = 'json';
- xhr.timeout = 10000;
- xhr.open('POST', url, true);
- xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
- xhr.onreadystatechange = function () {
- if (xhr.readyState === 4) {
- if (xhr.status !== 200) {
- count.innerText = count.innerText - 1 + 2;
- tile.style.display = '';
- }
+ helpers.xhr('POST', url, {payload: payload}, {
+ onNon200: function (xhr) {
+ count.textContent++;
+ tile.style.display = '';
}
- };
-
- xhr.send('csrf_token=' + watched_data.csrf_token);
+ });
}
diff --git a/scripts/deploy-database.sh b/scripts/deploy-database.sh
new file mode 100644
index 00000000..ed9464e6
--- /dev/null
+++ b/scripts/deploy-database.sh
@@ -0,0 +1,60 @@
+#!/bin/sh
+
+#
+# Parameters
+#
+
+interactive=true
+
+if [ "$1" == "--no-interactive" ]; then
+ interactive=false
+fi
+
+#
+# Enable and start Postgres
+#
+
+sudo systemctl start postgresql.service
+sudo systemctl enable postgresql.service
+
+#
+# Create databse and user
+#
+
+if [ "$interactive" == "true" ]; then
+ sudo -u postgres -- createuser -P kemal
+ sudo -u postgres -- createdb -O kemal invidious
+else
+ # Generate a DB password
+ if [ -z "$POSTGRES_PASS" ]; then
+ echo "Generating database password"
+ POSTGRES_PASS=$(tr -dc 'A-Za-z0-9.;!?{[()]}\\/' < /dev/urandom | head -c16)
+ fi
+
+ # hostname:port:database:username:password
+ echo "Writing .pgpass"
+ echo "127.0.0.1:*:invidious:kemal:${POSTGRES_PASS}" > "$HOME/.pgpass"
+
+ sudo -u postgres -- psql -c "CREATE USER kemal WITH PASSWORD '$POSTGRES_PASS';"
+ sudo -u postgres -- psql -c "CREATE DATABASE invidious WITH OWNER kemal;"
+ sudo -u postgres -- psql -c "GRANT ALL ON DATABASE invidious TO kemal;"
+fi
+
+
+#
+# Instructions for modification of pg_hba.conf
+#
+
+if [ "$interactive" = "true" ]; then
+ echo
+ echo "-------------"
+ echo " NOTICE "
+ echo "-------------"
+ echo
+ echo "Make sure that your postgreSQL's pg_hba.conf file contains the follwong"
+ echo "lines before previous 'host' configurations:"
+ echo
+ echo "host invidious kemal 127.0.0.1/32 md5"
+ echo "host invidious kemal ::1/128 md5"
+ echo
+fi
diff --git a/scripts/install-dependencies.sh b/scripts/install-dependencies.sh
new file mode 100644
index 00000000..27e0bf15
--- /dev/null
+++ b/scripts/install-dependencies.sh
@@ -0,0 +1,174 @@
+#!/bin/sh
+#
+# Script that installs the various dependencies of invidious
+#
+# Dependencies:
+# - crystal => Language in which Invidious is developed
+# - postgres => Database server
+# - git => required to clone Invidious
+# - librsvg2-bin => For login captcha (provides 'rsvg-convert')
+#
+# - libssl-dev => Used by Crystal's SSL module (standard library)
+# - libxml2-dev => Used by Crystal's XML module (standard library)
+# - libyaml-dev => Used by Crystal's YAML module (standard library)
+# - libgmp-dev => Used by Crystal's BigNumbers module (standard library)
+# - libevent-dev => Used by crystal's internal scheduler (?)
+# - libpcre3-dev => Used by Crystal's regex engine (?)
+#
+# - libsqlite3-dev => Used to open .db files from NewPipe exports
+# - zlib1g-dev => TBD
+# - libreadline-dev => TBD
+#
+#
+# Tested on:
+# - OpenSUSE Leap 15.3
+
+#
+# Load system details
+#
+
+if [ -e /etc/os-release ]; then
+ . /etc/os-release
+elif [ -e /usr/lib/os-release ]; then
+ . /usr/lib/os-release
+else
+ echo "Unsupported Linux system"
+ exit 2
+fi
+
+#
+# Some variables
+#
+
+repo_base_url="https://download.opensuse.org/repositories/devel:/languages:/crystal/"
+repo_end_url="devel:languages:crystal.repo"
+
+apt_gpg_key="/usr/share/keyrings/crystal.gpg"
+apt_list_file="/etc/apt/sources.list.d/crystal.list"
+
+yum_repo_file="/etc/yum.repos.d/crystal.repo"
+
+#
+# Major install functions
+#
+
+make_repo_url() {
+ echo "${repo_base_url}/${1}/${repo_end_url}"
+}
+
+
+install_apt() {
+ repo="$1"
+
+ echo "Adding Crystal repository"
+
+ curl -fsSL "${repo_base_url}/${repo}/Release.key" \
+ | gpg --dearmor \
+ | sudo tee "${apt_gpg_key}" > /dev/null
+
+ echo "deb [signed-by=${apt_gpg_key}] ${repo_base_url}/${repo}/ /" \
+ | sudo tee "$apt_list_file"
+
+ sudo apt-get update
+
+ sudo apt-get install --yes --no-install-recommends \
+ libssl-dev libxml2-dev libyaml-dev libgmp-dev libevent-dev \
+ libpcre3-dev libreadline-dev libsqlite3-dev zlib1g-dev \
+ crystal postgres git librsvg2-bin make
+}
+
+install_yum() {
+ repo=$(make_repo_url "$1")
+
+ echo "Adding Crystal repository"
+
+ cat << END | sudo tee "${yum_repo_file}" > /dev/null
+[crystal]
+name=Crystal
+type=rpm-md
+baseurl=${repo}/
+gpgcheck=1
+gpgkey=${repo}/repodata/repomd.xml.key
+enabled=1
+END
+
+ sudo yum -y install \
+ openssl-devel libxml2-devel libyaml-devel gmp-devel \
+ readline-devel sqlite-devel \
+ crystal postgresql postgresql-server git librsvg2-tools make
+}
+
+install_pacman() {
+ # TODO: find an alternative to --no-confirm?
+ sudo pacman -S --no-confirm \
+ base-devel librsvg postgresql crystal
+}
+
+install_zypper()
+{
+ repo=$(make_repo_url "$1")
+
+ echo "Adding Crystal repository"
+ sudo zypper --non-interactive addrepo -f "$repo"
+
+ sudo zypper --non-interactive --gpg-auto-import-keys install --no-recommends \
+ libopenssl-devel libxml2-devel libyaml-devel gmp-devel libevent-devel \
+ pcre-devel readline-devel sqlite3-devel zlib-devel \
+ crystal postgresql postgresql-server git rsvg-convert make
+}
+
+
+#
+# System-specific logic
+#
+
+case "$ID" in
+ archlinux) install_pacman;;
+
+ centos) install_dnf "CentOS_${VERSION_ID}";;
+
+ debian)
+ case "$VERSION_CODENAME" in
+ sid) install_apt "Debian_Unstable";;
+ bookworm) install_apt "Debian_Testing";;
+ *) install_apt "Debian_${VERSION_ID}";;
+ esac
+ ;;
+
+ fedora)
+ if [ "$VERSION" == *"Prerelease"* ]; then
+ install_dnf "Fedora_Rawhide"
+ else
+ install_dnf "Fedora_${VERSION}"
+ fi
+ ;;
+
+ opensuse-leap) install_zypper "openSUSE_Leap_${VERSION}";;
+
+ opensuse-tumbleweed) install_zypper "openSUSE_Tumbleweed";;
+
+ rhel) install_dnf "RHEL_${VERSION_ID}";;
+
+ ubuntu)
+ # Small workaround for recently released 22.04
+ case "$VERSION_ID" in
+ 22.04) install_apt "xUbuntu_21.04";;
+ *) install_apt "xUbuntu_${VERSION_ID}";;
+ esac
+ ;;
+
+ *)
+ # Try to match on ID_LIKE instead
+ # Not guaranteed to 100% work
+ case "$ID_LIKE" in
+ archlinux) install_pacman;;
+ centos) install_dnf "CentOS_${VERSION_ID}";;
+ debian) install_apt "Debian_${VERSION_ID}";;
+ *)
+ echo "Error: distribution ${CODENAME} is not supported"
+ echo "Please install dependencies manually"
+ exit 2
+ ;;
+ esac
+ ;;
+esac
diff --git a/src/invidious/comments.cr b/src/invidious/comments.cr
index 1aa14935..593189fd 100644
--- a/src/invidious/comments.cr
+++ b/src/invidious/comments.cr
@@ -481,7 +481,7 @@ def template_reddit_comments(root, locale)
html << <<-END_HTML
<p>
- <a href="javascript:void(0)" data-onclick="toggle_parent">[ - ]</a>
+ <a href="javascript:void(0)" data-onclick="toggle_parent">[ − ]</a>
<b><a href="https://www.reddit.com/user/#{child.author}">#{child.author}</a></b>
#{translate_count(locale, "comments_points_count", child.score, NumberFormatting::Separator)}
<span title="#{child.created_utc.to_s(translate(locale, "%a %B %-d %T %Y UTC"))}">#{translate(locale, "`x` ago", recode_date(child.created_utc, locale))}</span>
diff --git a/src/invidious/views/licenses.ecr b/src/invidious/views/licenses.ecr
index 861913d0..25b24ed4 100644
--- a/src/invidious/views/licenses.ecr
+++ b/src/invidious/views/licenses.ecr
@@ -11,6 +11,20 @@
<table id="jslicense-labels1">
<tr>
<td>
+ <a href="/js/_helpers.js?v=<%= ASSET_COMMIT %>">_helpers.js</a>
+ </td>
+
+ <td>
+ <a href="https://www.gnu.org/licenses/agpl-3.0.html">AGPL-3.0</a>
+ </td>
+
+ <td>
+ <a href="/js/_helpers.js?v=<%= ASSET_COMMIT %>"><%= translate(locale, "source") %></a>
+ </td>
+ </tr>
+
+ <tr>
+ <td>
<a href="/js/community.js?v=<%= ASSET_COMMIT %>">community.js</a>
</td>
diff --git a/src/invidious/views/template.ecr b/src/invidious/views/template.ecr
index bd908dd6..4e2b29f0 100644
--- a/src/invidious/views/template.ecr
+++ b/src/invidious/views/template.ecr
@@ -17,6 +17,7 @@
<link rel="stylesheet" href="/css/grids-responsive-min.css?v=<%= ASSET_COMMIT %>">
<link rel="stylesheet" href="/css/ionicons.min.css?v=<%= ASSET_COMMIT %>">
<link rel="stylesheet" href="/css/default.css?v=<%= ASSET_COMMIT %>">
+ <script src="/js/_helpers.js?v=<%= ASSET_COMMIT %>"></script>
</head>
<%
diff --git a/src/invidious/views/watch.ecr b/src/invidious/views/watch.ecr
index 8b6eb903..d1fdcce2 100644
--- a/src/invidious/views/watch.ecr
+++ b/src/invidious/views/watch.ecr
@@ -173,7 +173,7 @@ we're going to need to do it here in order to allow for translations.
<p id="views"><i class="icon ion-ios-eye"></i> <%= number_with_separator(video.views) %></p>
<p id="likes"><i class="icon ion-ios-thumbs-up"></i> <%= number_with_separator(video.likes) %></p>
- <p id="dislikes"><i class="icon ion-ios-thumbs-down"></i> <%= number_with_separator(video.dislikes) %></p>
+ <p id="dislikes"></p>
<p id="genre"><%= translate(locale, "Genre: ") %>
<% if !video.genre_url %>
<%= video.genre %>
@@ -186,7 +186,7 @@ we're going to need to do it here in order to allow for translations.
<% end %>
<p id="family_friendly"><%= translate(locale, "Family friendly? ") %><%= translate_bool(locale, video.is_family_friendly) %></p>
<p id="wilson"><%= translate(locale, "Wilson score: ") %><%= video.wilson_score %></p>
- <p id="rating"><%= translate(locale, "Rating: ") %><%= video.average_rating %> / 5</p>
+ <p id="rating"></p>
<p id="engagement"><%= translate(locale, "Engagement: ") %><%= video.engagement %>%</p>
<% if video.allowed_regions.size != REGIONS.size %>
<p id="allowed_regions">
@@ -278,24 +278,24 @@ we're going to need to do it here in order to allow for translations.
</div>
<% end %>
<p style="width:100%"><%= rv["title"] %></p>
- <h5 class="pure-g">
- <div class="pure-u-14-24">
- <% if rv["ucid"]? %>
- <b style="width:100%"><a href="/channel/<%= rv["ucid"] %>"><%= rv["author"]? %><% if rv["author_verified"]? == "true" %>&nbsp;<i class="icon ion ion-md-checkmark-circle"></i><% end %></a></b>
- <% else %>
- <b style="width:100%"><%= rv["author"]? %><% if rv["author_verified"]? == "true" %>&nbsp;<i class="icon ion ion-md-checkmark-circle"></i><% end %></b>
- <% end %>
- </div>
-
- <div class="pure-u-10-24" style="text-align:right">
- <b class="width:100%"><%=
- views = rv["view_count"]?.try &.to_i?
- views ||= rv["view_count_short"]?.try { |x| short_text_to_number(x) }
- translate_count(locale, "generic_views_count", views || 0, NumberFormatting::Short)
- %></b>
- </div>
- </h5>
</a>
+ <h5 class="pure-g">
+ <div class="pure-u-14-24">
+ <% if rv["ucid"]? %>
+ <b style="width:100%"><a href="/channel/<%= rv["ucid"] %>"><%= rv["author"]? %><% if rv["author_verified"]? == "true" %>&nbsp;<i class="icon ion ion-md-checkmark-circle"></i><% end %></a></b>
+ <% else %>
+ <b style="width:100%"><%= rv["author"]? %><% if rv["author_verified"]? == "true" %>&nbsp;<i class="icon ion ion-md-checkmark-circle"></i><% end %></b>
+ <% end %>
+ </div>
+
+ <div class="pure-u-10-24" style="text-align:right">
+ <b class="width:100%"><%=
+ views = rv["view_count"]?.try &.to_i?
+ views ||= rv["view_count_short"]?.try { |x| short_text_to_number(x) }
+ translate_count(locale, "generic_views_count", views || 0, NumberFormatting::Short)
+ %></b>
+ </div>
+ </h5>
<% end %>
<% end %>
</div>