Merge branch 'master' of github.com:iv-org/invidious
このコミットが含まれているのは:
コミット
33e2e71e1c
|
@ -46,15 +46,17 @@ jobs:
|
||||||
stable: false
|
stable: false
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
submodules: true
|
||||||
|
|
||||||
- name: Install Crystal
|
- name: Install Crystal
|
||||||
uses: crystal-lang/install-crystal@v1.5.3
|
uses: crystal-lang/install-crystal@v1.6.0
|
||||||
with:
|
with:
|
||||||
crystal: ${{ matrix.crystal }}
|
crystal: ${{ matrix.crystal }}
|
||||||
|
|
||||||
- name: Cache Shards
|
- name: Cache Shards
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v3
|
||||||
with:
|
with:
|
||||||
path: ./lib
|
path: ./lib
|
||||||
key: shards-${{ hashFiles('shard.lock') }}
|
key: shards-${{ hashFiles('shard.lock') }}
|
||||||
|
@ -84,7 +86,7 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Build Docker
|
- name: Build Docker
|
||||||
run: docker-compose build --build-arg release=0
|
run: docker-compose build --build-arg release=0
|
||||||
|
@ -100,18 +102,18 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v1
|
uses: docker/setup-qemu-action@v2
|
||||||
with:
|
with:
|
||||||
platforms: arm64
|
platforms: arm64
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v1
|
uses: docker/setup-buildx-action@v2
|
||||||
|
|
||||||
- name: Build Docker ARM64 image
|
- name: Build Docker ARM64 image
|
||||||
uses: docker/build-push-action@v2
|
uses: docker/build-push-action@v3
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: docker/Dockerfile.arm64
|
file: docker/Dockerfile.arm64
|
||||||
|
|
|
@ -15,20 +15,20 @@ on:
|
||||||
- screenshots/*
|
- screenshots/*
|
||||||
- .github/ISSUE_TEMPLATE/*
|
- .github/ISSUE_TEMPLATE/*
|
||||||
- kubernetes/**
|
- kubernetes/**
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Install Crystal
|
- name: Install Crystal
|
||||||
uses: oprypin/install-crystal@v1.2.4
|
uses: crystal-lang/install-crystal@v1.6.0
|
||||||
with:
|
with:
|
||||||
crystal: 1.2.2
|
crystal: 1.2.2
|
||||||
|
|
||||||
- name: Run lint
|
- name: Run lint
|
||||||
run: |
|
run: |
|
||||||
if ! crystal tool format --check; then
|
if ! crystal tool format --check; then
|
||||||
|
@ -38,15 +38,15 @@ jobs:
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v1
|
uses: docker/setup-qemu-action@v2
|
||||||
with:
|
with:
|
||||||
platforms: arm64
|
platforms: arm64
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v1
|
uses: docker/setup-buildx-action@v2
|
||||||
|
|
||||||
- name: Login to registry
|
- name: Login to registry
|
||||||
uses: docker/login-action@v1
|
uses: docker/login-action@v2
|
||||||
with:
|
with:
|
||||||
registry: quay.io
|
registry: quay.io
|
||||||
username: ${{ secrets.QUAY_USERNAME }}
|
username: ${{ secrets.QUAY_USERNAME }}
|
||||||
|
@ -54,7 +54,7 @@ jobs:
|
||||||
|
|
||||||
- name: Build and push Docker AMD64 image for Push Event
|
- name: Build and push Docker AMD64 image for Push Event
|
||||||
if: github.ref == 'refs/heads/master'
|
if: github.ref == 'refs/heads/master'
|
||||||
uses: docker/build-push-action@v2
|
uses: docker/build-push-action@v3
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: docker/Dockerfile
|
file: docker/Dockerfile
|
||||||
|
@ -66,7 +66,7 @@ jobs:
|
||||||
|
|
||||||
- name: Build and push Docker ARM64 image for Push Event
|
- name: Build and push Docker ARM64 image for Push Event
|
||||||
if: github.ref == 'refs/heads/master'
|
if: github.ref == 'refs/heads/master'
|
||||||
uses: docker/build-push-action@v2
|
uses: docker/build-push-action@v3
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: docker/Dockerfile.arm64
|
file: docker/Dockerfile.arm64
|
||||||
|
|
|
@ -10,11 +10,11 @@ jobs:
|
||||||
stale:
|
stale:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@v3
|
- uses: actions/stale@v5
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
days-before-stale: 365
|
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
|
days-before-close: 30
|
||||||
exempt-pr-labels: blocked
|
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.'
|
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.'
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
[submodule "mocks"]
|
||||||
|
path = mocks
|
||||||
|
url = ../mocks
|
|
@ -152,6 +152,7 @@ Weblate also allows you to log-in with major SSO providers like Github, Gitlab,
|
||||||
- [HoloPlay](https://github.com/stephane-r/HoloPlay): Funny Android application connecting on Invidious API's with search, playlists and favorites.
|
- [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.
|
- [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.
|
- [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
|
## Liability
|
||||||
|
|
|
@ -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';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})()
|
||||||
|
};
|
|
@ -1,13 +1,6 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
var community_data = JSON.parse(document.getElementById('community_data').textContent);
|
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) {
|
function hide_youtube_replies(event) {
|
||||||
var target = event.target;
|
var target = event.target;
|
||||||
|
|
||||||
|
@ -38,13 +31,6 @@ function show_youtube_replies(event) {
|
||||||
target.setAttribute('data-sub-text', sub_text);
|
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) {
|
function get_youtube_replies(target, load_more) {
|
||||||
var continuation = target.getAttribute('data-continuation');
|
var continuation = target.getAttribute('data-continuation');
|
||||||
|
|
||||||
|
@ -58,47 +44,39 @@ function get_youtube_replies(target, load_more) {
|
||||||
'&hl=' + community_data.preferences.locale +
|
'&hl=' + community_data.preferences.locale +
|
||||||
'&thin_mode=' + community_data.preferences.thin_mode +
|
'&thin_mode=' + community_data.preferences.thin_mode +
|
||||||
'&continuation=' + continuation;
|
'&continuation=' + continuation;
|
||||||
var xhr = new XMLHttpRequest();
|
|
||||||
xhr.responseType = 'json';
|
|
||||||
xhr.timeout = 10000;
|
|
||||||
xhr.open('GET', url, true);
|
|
||||||
|
|
||||||
xhr.onreadystatechange = function () {
|
helpers.xhr('GET', url, {}, {
|
||||||
if (xhr.readyState === 4) {
|
on200: function (response) {
|
||||||
if (xhr.status === 200) {
|
if (load_more) {
|
||||||
if (load_more) {
|
body = body.parentNode.parentNode;
|
||||||
body = body.parentNode.parentNode;
|
body.removeChild(body.lastElementChild);
|
||||||
body.removeChild(body.lastElementChild);
|
body.innerHTML += response.contentHtml;
|
||||||
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);
|
|
||||||
}
|
|
||||||
} else {
|
} 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;
|
||||||
|
|
||||||
|
var div = document.createElement('div');
|
||||||
|
div.innerHTML = response.contentHtml;
|
||||||
|
|
||||||
|
body.appendChild(p);
|
||||||
|
body.appendChild(div);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
onNon200: function (xhr) {
|
||||||
|
body.innerHTML = fallback;
|
||||||
|
},
|
||||||
|
onTimeout: function (xhr) {
|
||||||
|
console.warn('Pulling comments failed');
|
||||||
|
body.innerHTML = fallback;
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
xhr.ontimeout = function () {
|
|
||||||
console.warn('Pulling comments failed.');
|
|
||||||
body.innerHTML = fallback;
|
|
||||||
};
|
|
||||||
|
|
||||||
xhr.send();
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,7 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
var video_data = JSON.parse(document.getElementById('video_data').textContent);
|
var video_data = JSON.parse(document.getElementById('video_data').textContent);
|
||||||
|
|
||||||
function get_playlist(plid, retries) {
|
function get_playlist(plid) {
|
||||||
if (retries === undefined) retries = 5;
|
|
||||||
|
|
||||||
if (retries <= 0) {
|
|
||||||
console.warn('Failed to pull playlist');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var plid_url;
|
var plid_url;
|
||||||
if (plid.startsWith('RD')) {
|
if (plid.startsWith('RD')) {
|
||||||
plid_url = '/api/v1/mixes/' + plid +
|
plid_url = '/api/v1/mixes/' + plid +
|
||||||
|
@ -21,85 +14,49 @@ function get_playlist(plid, retries) {
|
||||||
'&format=html&hl=' + video_data.preferences.locale;
|
'&format=html&hl=' + video_data.preferences.locale;
|
||||||
}
|
}
|
||||||
|
|
||||||
var xhr = new XMLHttpRequest();
|
helpers.xhr('GET', plid_url, {retries: 5, entity_name: 'playlist'}, {
|
||||||
xhr.responseType = 'json';
|
on200: function (response) {
|
||||||
xhr.timeout = 10000;
|
if (!response.nextVideo)
|
||||||
xhr.open('GET', plid_url, true);
|
return;
|
||||||
|
|
||||||
xhr.onreadystatechange = function () {
|
player.on('ended', function () {
|
||||||
if (xhr.readyState === 4) {
|
var url = new URL('https://example.com/embed/' + response.nextVideo);
|
||||||
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);
|
url.searchParams.set('list', plid);
|
||||||
if (!plid.startsWith('RD')) {
|
if (!plid.startsWith('RD'))
|
||||||
url.searchParams.set('index', xhr.response.index);
|
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);
|
||||||
|
|
||||||
if (video_data.params.autoplay || video_data.params.continue_autoplay) {
|
location.assign(url.pathname + url.search);
|
||||||
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) {
|
if (video_data.plid) {
|
||||||
get_playlist(video_data.plid);
|
get_playlist(video_data.plid);
|
||||||
} else if (video_data.video_series) {
|
} else if (video_data.video_series) {
|
||||||
player.on('ended', function () {
|
player.on('ended', function () {
|
||||||
var url = new URL('https://example.com/embed/' + video_data.video_series.shift());
|
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');
|
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);
|
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);
|
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('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(','));
|
url.searchParams.set('playlist', video_data.video_series.join(','));
|
||||||
}
|
|
||||||
|
|
||||||
location.assign(url.pathname + url.search);
|
location.assign(url.pathname + url.search);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
(function () {
|
(function () {
|
||||||
var n2a = function (n) { return Array.prototype.slice.call(n); };
|
|
||||||
|
|
||||||
var video_player = document.getElementById('player_html5_api');
|
var video_player = document.getElementById('player_html5_api');
|
||||||
if (video_player) {
|
if (video_player) {
|
||||||
video_player.onmouseenter = function () { video_player['data-title'] = video_player['title']; video_player['title'] = ''; };
|
video_player.onmouseenter = function () { video_player['data-title'] = video_player['title']; video_player['title'] = ''; };
|
||||||
|
@ -11,8 +9,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// For dynamically inserted elements
|
// For dynamically inserted elements
|
||||||
document.addEventListener('click', function (e) {
|
addEventListener('click', function (e) {
|
||||||
if (!e || !e.target) { return; }
|
if (!e || !e.target) return;
|
||||||
|
|
||||||
var t = e.target;
|
var t = e.target;
|
||||||
var handler_name = t.getAttribute('data-onclick');
|
var handler_name = t.getAttribute('data-onclick');
|
||||||
|
@ -29,6 +27,7 @@
|
||||||
get_youtube_replies(t, load_more, load_replies);
|
get_youtube_replies(t, load_more, load_replies);
|
||||||
break;
|
break;
|
||||||
case 'toggle_parent':
|
case 'toggle_parent':
|
||||||
|
e.preventDefault();
|
||||||
toggle_parent(t);
|
toggle_parent(t);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
@ -36,118 +35,98 @@
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
n2a(document.querySelectorAll('[data-mouse="switch_classes"]')).forEach(function (e) {
|
document.querySelectorAll('[data-mouse="switch_classes"]').forEach(function (el) {
|
||||||
var classes = e.getAttribute('data-switch-classes').split(',');
|
var classes = el.getAttribute('data-switch-classes').split(',');
|
||||||
var ec = classes[0];
|
var classOnEnter = classes[0];
|
||||||
var lc = classes[1];
|
var classOnLeave = classes[1];
|
||||||
var onoff = function (on, off) {
|
function toggle_classes(toAdd, toRemove) {
|
||||||
var cs = e.getAttribute('class');
|
el.classList.add(toAdd);
|
||||||
cs = cs.split(off).join(on);
|
el.classList.remove(toRemove);
|
||||||
e.setAttribute('class', cs);
|
}
|
||||||
};
|
el.onmouseenter = function () { toggle_classes(classOnEnter, classOnLeave); };
|
||||||
e.onmouseenter = function () { onoff(ec, lc); };
|
el.onmouseleave = function () { toggle_classes(classOnLeave, classOnEnter); };
|
||||||
e.onmouseleave = function () { onoff(lc, ec); };
|
|
||||||
});
|
});
|
||||||
|
|
||||||
n2a(document.querySelectorAll('[data-onsubmit="return_false"]')).forEach(function (e) {
|
document.querySelectorAll('[data-onsubmit="return_false"]').forEach(function (el) {
|
||||||
e.onsubmit = function () { return false; };
|
el.onsubmit = function () { return false; };
|
||||||
});
|
});
|
||||||
|
|
||||||
n2a(document.querySelectorAll('[data-onclick="mark_watched"]')).forEach(function (e) {
|
document.querySelectorAll('[data-onclick="mark_watched"]').forEach(function (el) {
|
||||||
e.onclick = function () { mark_watched(e); };
|
el.onclick = function () { mark_watched(el); };
|
||||||
});
|
});
|
||||||
n2a(document.querySelectorAll('[data-onclick="mark_unwatched"]')).forEach(function (e) {
|
document.querySelectorAll('[data-onclick="mark_unwatched"]').forEach(function (el) {
|
||||||
e.onclick = function () { mark_unwatched(e); };
|
el.onclick = function () { mark_unwatched(el); };
|
||||||
});
|
});
|
||||||
n2a(document.querySelectorAll('[data-onclick="add_playlist_video"]')).forEach(function (e) {
|
document.querySelectorAll('[data-onclick="add_playlist_video"]').forEach(function (el) {
|
||||||
e.onclick = function () { add_playlist_video(e); };
|
el.onclick = function () { add_playlist_video(el); };
|
||||||
});
|
});
|
||||||
n2a(document.querySelectorAll('[data-onclick="add_playlist_item"]')).forEach(function (e) {
|
document.querySelectorAll('[data-onclick="add_playlist_item"]').forEach(function (el) {
|
||||||
e.onclick = function () { add_playlist_item(e); };
|
el.onclick = function () { add_playlist_item(el); };
|
||||||
});
|
});
|
||||||
n2a(document.querySelectorAll('[data-onclick="remove_playlist_item"]')).forEach(function (e) {
|
document.querySelectorAll('[data-onclick="remove_playlist_item"]').forEach(function (el) {
|
||||||
e.onclick = function () { remove_playlist_item(e); };
|
el.onclick = function () { remove_playlist_item(el); };
|
||||||
});
|
});
|
||||||
n2a(document.querySelectorAll('[data-onclick="revoke_token"]')).forEach(function (e) {
|
document.querySelectorAll('[data-onclick="revoke_token"]').forEach(function (el) {
|
||||||
e.onclick = function () { revoke_token(e); };
|
el.onclick = function () { revoke_token(el); };
|
||||||
});
|
});
|
||||||
n2a(document.querySelectorAll('[data-onclick="remove_subscription"]')).forEach(function (e) {
|
document.querySelectorAll('[data-onclick="remove_subscription"]').forEach(function (el) {
|
||||||
e.onclick = function () { remove_subscription(e); };
|
el.onclick = function () { remove_subscription(el); };
|
||||||
});
|
});
|
||||||
n2a(document.querySelectorAll('[data-onclick="notification_requestPermission"]')).forEach(function (e) {
|
document.querySelectorAll('[data-onclick="notification_requestPermission"]').forEach(function (el) {
|
||||||
e.onclick = function () { Notification.requestPermission(); };
|
el.onclick = function () { Notification.requestPermission(); };
|
||||||
});
|
});
|
||||||
|
|
||||||
n2a(document.querySelectorAll('[data-onrange="update_volume_value"]')).forEach(function (e) {
|
document.querySelectorAll('[data-onrange="update_volume_value"]').forEach(function (el) {
|
||||||
var cb = function () { update_volume_value(e); };
|
function update_volume_value() {
|
||||||
e.oninput = cb;
|
document.getElementById('volume-value').textContent = el.value;
|
||||||
e.onchange = cb;
|
}
|
||||||
|
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) {
|
function revoke_token(target) {
|
||||||
var row = target.parentNode.parentNode.parentNode.parentNode.parentNode;
|
var row = target.parentNode.parentNode.parentNode.parentNode.parentNode;
|
||||||
row.style.display = 'none';
|
row.style.display = 'none';
|
||||||
var count = document.getElementById('count');
|
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' +
|
var url = '/token_ajax?action_revoke_token=1&redirect=false' +
|
||||||
'&referer=' + referer +
|
'&referer=' + encodeURIComponent(location.href) +
|
||||||
'&session=' + target.getAttribute('data-session');
|
'&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 () {
|
var payload = 'csrf_token=' + target.parentNode.querySelector('input[name="csrf_token"]').value;
|
||||||
if (xhr.readyState === 4) {
|
|
||||||
if (xhr.status !== 200) {
|
helpers.xhr('POST', url, {payload: payload}, {
|
||||||
count.innerText = parseInt(count.innerText) + 1;
|
onNon200: function (xhr) {
|
||||||
row.style.display = '';
|
count.textContent++;
|
||||||
}
|
row.style.display = '';
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
var csrf_token = target.parentNode.querySelector('input[name="csrf_token"]').value;
|
|
||||||
xhr.send('csrf_token=' + csrf_token);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function remove_subscription(target) {
|
function remove_subscription(target) {
|
||||||
var row = target.parentNode.parentNode.parentNode.parentNode.parentNode;
|
var row = target.parentNode.parentNode.parentNode.parentNode.parentNode;
|
||||||
row.style.display = 'none';
|
row.style.display = 'none';
|
||||||
var count = document.getElementById('count');
|
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' +
|
var url = '/subscription_ajax?action_remove_subscriptions=1&redirect=false' +
|
||||||
'&referer=' + referer +
|
'&referer=' + encodeURIComponent(location.href) +
|
||||||
'&c=' + target.getAttribute('data-ucid');
|
'&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 () {
|
var payload = 'csrf_token=' + target.parentNode.querySelector('input[name="csrf_token"]').value;
|
||||||
if (xhr.readyState === 4) {
|
|
||||||
if (xhr.status !== 200) {
|
helpers.xhr('POST', url, {payload: payload}, {
|
||||||
count.innerText = parseInt(count.innerText) + 1;
|
onNon200: function (xhr) {
|
||||||
row.style.display = '';
|
count.textContent++;
|
||||||
}
|
row.style.display = '';
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
var csrf_token = target.parentNode.querySelector('input[name="csrf_token"]').value;
|
|
||||||
xhr.send('csrf_token=' + csrf_token);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle keypresses
|
// Handle keypresses
|
||||||
window.addEventListener('keydown', function (event) {
|
addEventListener('keydown', function (event) {
|
||||||
// Ignore modifier keys
|
// Ignore modifier keys
|
||||||
if (event.ctrlKey || event.metaKey) return;
|
if (event.ctrlKey || event.metaKey) return;
|
||||||
|
|
||||||
|
|
|
@ -1,43 +1,26 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
var notification_data = JSON.parse(document.getElementById('notification_data').textContent);
|
var notification_data = JSON.parse(document.getElementById('notification_data').textContent);
|
||||||
|
|
||||||
|
/** 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';
|
||||||
|
|
||||||
var notifications, delivered;
|
var notifications, delivered;
|
||||||
|
var notifications_mock = { close: function () { } };
|
||||||
|
|
||||||
function get_subscriptions(callback, retries) {
|
function get_subscriptions() {
|
||||||
if (retries === undefined) retries = 5;
|
helpers.xhr('GET', '/api/v1/auth/subscriptions?fields=authorId', {
|
||||||
|
retries: 5,
|
||||||
if (retries <= 0) {
|
entity_name: 'subscriptions'
|
||||||
return;
|
}, {
|
||||||
}
|
on200: create_notification_stream
|
||||||
|
});
|
||||||
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);
|
|
||||||
};
|
|
||||||
|
|
||||||
xhr.ontimeout = function () {
|
|
||||||
console.warn('Pulling subscriptions failed... ' + retries + '/5');
|
|
||||||
get_subscriptions(callback, retries - 1);
|
|
||||||
};
|
|
||||||
|
|
||||||
xhr.send();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function create_notification_stream(subscriptions) {
|
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(
|
notifications = new SSE(
|
||||||
'/api/v1/auth/notifications?fields=videoId,title,author,authorId,publishedText,published,authorThumbnails,liveNow', {
|
'/api/v1/auth/notifications?fields=videoId,title,author,authorId,publishedText,published,authorThumbnails,liveNow', {
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
|
@ -49,96 +32,100 @@ function create_notification_stream(subscriptions) {
|
||||||
var start_time = Math.round(new Date() / 1000);
|
var start_time = Math.round(new Date() / 1000);
|
||||||
|
|
||||||
notifications.onmessage = function (event) {
|
notifications.onmessage = function (event) {
|
||||||
if (!event.id) {
|
if (!event.id) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var notification = JSON.parse(event.data);
|
var notification = JSON.parse(event.data);
|
||||||
console.info('Got notification:', notification);
|
console.info('Got notification:', notification);
|
||||||
|
|
||||||
if (start_time < notification.published && !delivered.includes(notification.videoId)) {
|
// Ignore not actual and delivered notifications
|
||||||
if (Notification.permission === 'granted') {
|
if (start_time > notification.published || delivered.includes(notification.videoId)) return;
|
||||||
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) {
|
delivered.push(notification.videoId);
|
||||||
window.open('/watch?v=' + event.currentTarget.tag, '_blank');
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
delivered.push(notification.videoId);
|
let notification_count = helpers.storage.get(STORAGE_KEY_NOTIF_COUNT) || 0;
|
||||||
localStorage.setItem('notification_count', parseInt(localStorage.getItem('notification_count') || '0') + 1);
|
notification_count++;
|
||||||
var notification_ticker = document.getElementById('notification_ticker');
|
helpers.storage.set(STORAGE_KEY_NOTIF_COUNT, notification_count);
|
||||||
|
|
||||||
if (parseInt(localStorage.getItem('notification_count')) > 0) {
|
update_ticker_count();
|
||||||
notification_ticker.innerHTML =
|
|
||||||
'<span id="notification_count">' + localStorage.getItem('notification_count') + '</span> <i class="icon ion-ios-notifications"></i>';
|
// permission for notifications handled on settings page. JS handler is in handlers.js
|
||||||
} else {
|
if (window.Notification && Notification.permission === 'granted') {
|
||||||
notification_ticker.innerHTML =
|
var notification_text = notification.liveNow ? notification_data.live_now_text : notification_data.upload_text;
|
||||||
'<i class="icon ion-ios-notifications-outline"></i>';
|
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.addEventListener('error', function (e) {
|
||||||
|
console.warn('Something went wrong with notifications, trying to reconnect...');
|
||||||
|
notifications = notifications_mock;
|
||||||
|
setTimeout(get_subscriptions, 1000);
|
||||||
|
});
|
||||||
|
|
||||||
notifications.stream();
|
notifications.stream();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handle_notification_error(event) {
|
function update_ticker_count() {
|
||||||
console.warn('Something went wrong with notifications, trying to reconnect...');
|
var notification_ticker = document.getElementById('notification_ticker');
|
||||||
notifications = { close: function () { } };
|
|
||||||
setTimeout(function () { get_subscriptions(create_notification_stream); }, 1000);
|
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 {
|
||||||
|
notification_ticker.innerHTML =
|
||||||
|
'<i class="icon ion-ios-notifications-outline"></i>';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('load', function (e) {
|
function start_stream_if_needed() {
|
||||||
localStorage.setItem('notification_count', document.getElementById('notification_count') ? document.getElementById('notification_count').innerText : '0');
|
// random wait for other tabs set 'stream' flag
|
||||||
|
setTimeout(function () {
|
||||||
if (localStorage.getItem('stream')) {
|
if (!helpers.storage.get(STORAGE_KEY_STREAM)) {
|
||||||
localStorage.removeItem('stream');
|
// if no one set 'stream', set it by yourself and start stream
|
||||||
} else {
|
helpers.storage.set(STORAGE_KEY_STREAM, true);
|
||||||
setTimeout(function () {
|
notifications = notifications_mock;
|
||||||
if (!localStorage.getItem('stream')) {
|
get_subscriptions();
|
||||||
notifications = { close: function () { } };
|
|
||||||
localStorage.setItem('stream', true);
|
|
||||||
get_subscriptions(create_notification_stream);
|
|
||||||
}
|
|
||||||
}, Math.random() * 1000 + 50);
|
|
||||||
}
|
|
||||||
|
|
||||||
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>';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
}, Math.random() * 1000 + 50); // [0.050 .. 1.050) second
|
||||||
});
|
}
|
||||||
|
|
||||||
window.addEventListener('unload', function (e) {
|
|
||||||
if (notifications) {
|
addEventListener('storage', function (e) {
|
||||||
localStorage.removeItem('stream');
|
if (e.key === STORAGE_KEY_NOTIF_COUNT)
|
||||||
|
update_ticker_count();
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
});
|
||||||
|
|
|
@ -42,45 +42,53 @@ embed_url = location.origin + '/embed/' + video_data.id + embed_url.search;
|
||||||
var save_player_pos_key = 'save_player_pos';
|
var save_player_pos_key = 'save_player_pos';
|
||||||
|
|
||||||
videojs.Vhs.xhr.beforeRequest = function(options) {
|
videojs.Vhs.xhr.beforeRequest = function(options) {
|
||||||
if (options.uri.indexOf('videoplayback') === -1 && options.uri.indexOf('local=true') === -1) {
|
// set local if requested not videoplayback
|
||||||
options.uri = options.uri + '?local=true';
|
if (!options.uri.includes('videoplayback')) {
|
||||||
|
if (!options.uri.includes('local=true'))
|
||||||
|
options.uri += '?local=true';
|
||||||
}
|
}
|
||||||
return options;
|
return options;
|
||||||
};
|
};
|
||||||
|
|
||||||
var player = videojs('player', options);
|
var player = videojs('player', options);
|
||||||
|
|
||||||
player.on('error', () => {
|
player.on('error', function () {
|
||||||
if (video_data.params.quality !== 'dash') {
|
if (video_data.params.quality === 'dash') return;
|
||||||
if (!player.currentSrc().includes("local=true") && !video_data.local_disabled) {
|
|
||||||
var currentSources = player.currentSources();
|
var localNotDisabled = (
|
||||||
for (var i = 0; i < currentSources.length; i++) {
|
!player.currentSrc().includes('local=true') && !video_data.local_disabled
|
||||||
currentSources[i]["src"] += "&local=true"
|
);
|
||||||
}
|
var reloadMakesSense = (
|
||||||
player.src(currentSources)
|
player.error().code === MediaError.MEDIA_ERR_NETWORK ||
|
||||||
}
|
player.error().code === MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED
|
||||||
else if (player.error().code === 2 || player.error().code === 4) {
|
);
|
||||||
setTimeout(function (event) {
|
|
||||||
console.log('An error occurred in the player, reloading...');
|
if (localNotDisabled) {
|
||||||
|
// add local=true to all current sources
|
||||||
var currentTime = player.currentTime();
|
player.src(player.currentSources().map(function (source) {
|
||||||
var playbackRate = player.playbackRate();
|
source.src += '&local=true';
|
||||||
var paused = player.paused();
|
}));
|
||||||
|
} else if (reloadMakesSense) {
|
||||||
player.load();
|
setTimeout(function () {
|
||||||
|
console.warn('An error occurred in the player, reloading...');
|
||||||
if (currentTime > 0.5) currentTime -= 0.5;
|
|
||||||
|
// After load() all parameters are reset. Save them
|
||||||
player.currentTime(currentTime);
|
var currentTime = player.currentTime();
|
||||||
player.playbackRate(playbackRate);
|
var playbackRate = player.playbackRate();
|
||||||
|
var paused = player.paused();
|
||||||
if (!paused) player.play();
|
|
||||||
}, 10000);
|
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({
|
player.reloadSourceOnError({
|
||||||
errorInterval: 10
|
errorInterval: 10
|
||||||
});
|
});
|
||||||
|
@ -89,7 +97,7 @@ if (video_data.params.quality == 'dash') {
|
||||||
/**
|
/**
|
||||||
* Function for add time argument to url
|
* Function for add time argument to url
|
||||||
* @param {String} url
|
* @param {String} url
|
||||||
* @returns urlWithTimeArg
|
* @returns {URL} urlWithTimeArg
|
||||||
*/
|
*/
|
||||||
function addCurrentTimeToURL(url) {
|
function addCurrentTimeToURL(url) {
|
||||||
var urlUsed = new URL(url);
|
var urlUsed = new URL(url);
|
||||||
|
@ -112,18 +120,12 @@ var shareOptions = {
|
||||||
description: player_data.description,
|
description: player_data.description,
|
||||||
image: player_data.thumbnail,
|
image: player_data.thumbnail,
|
||||||
get embedCode() {
|
get embedCode() {
|
||||||
return '<iframe id="ivplayer" width="640" height="360" src="' +
|
// Single quotes inside here required. HTML inserted as is into value attribute of input
|
||||||
addCurrentTimeToURL(embed_url) + '" style="border:none;"></iframe>';
|
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/')) {
|
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>';
|
var overlay_content = '<h1><a rel="noopener" target="_blank" href="' + location.origin + '/watch?v=' + video_data.id + '">' + player_data.title + '</a></h1>';
|
||||||
player.overlay({
|
player.overlay({
|
||||||
|
@ -162,7 +164,7 @@ if (isMobile()) {
|
||||||
buttons.forEach(function (child) {primary_control_bar.removeChild(child);});
|
buttons.forEach(function (child) {primary_control_bar.removeChild(child);});
|
||||||
|
|
||||||
var operations_bar_element = operations_bar.el();
|
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);
|
player.addChild(operations_bar);
|
||||||
|
|
||||||
// Playback menu doesn't work when it's initialized outside of the primary control 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);
|
operations_bar_element.append(share_element);
|
||||||
|
|
||||||
if (video_data.params.quality === 'dash') {
|
if (video_data.params.quality === 'dash') {
|
||||||
var http_source_selector = document.getElementsByClassName('vjs-http-source-selector vjs-menu-button')[0];
|
var http_source_selector = document.getElementsByClassName('vjs-http-source-selector vjs-menu-button')[0];
|
||||||
operations_bar_element.append(http_source_selector);
|
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
|
* Method for getting the contents of a cookie
|
||||||
*
|
*
|
||||||
* @param {String} name Name of cookie
|
* @param {String} name Name of cookie
|
||||||
* @returns cookieValue
|
* @returns {String|null} cookieValue
|
||||||
*/
|
*/
|
||||||
function getCookieValue(name) {
|
function getCookieValue(name) {
|
||||||
var value = document.cookie.split(';').filter(function (item) {return item.includes(name + '=');});
|
var cookiePrefix = name + '=';
|
||||||
|
var matchedCookie = document.cookie.split(';').find(function (item) {return item.includes(cookiePrefix);});
|
||||||
return (value.length >= 1)
|
if (matchedCookie)
|
||||||
? value[0].substring((name + '=').length, value[0].length)
|
return matchedCookie.replace(cookiePrefix, '');
|
||||||
: null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -257,11 +259,11 @@ function updateCookie(newVolume, newSpeed) {
|
||||||
date.setTime(date.getTime() + 63115200);
|
date.setTime(date.getTime() + 63115200);
|
||||||
|
|
||||||
var ipRegex = /^((\d+\.){3}\d+|[A-Fa-f0-9]*:[A-Fa-f0-9:]*:[A-Fa-f0-9:]+)$/;
|
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
|
// 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')
|
if (domainUsed.charAt(0) !== '.' && !ipRegex.test(domainUsed) && domainUsed !== 'localhost')
|
||||||
domainUsed = '.' + window.location.hostname;
|
domainUsed = '.' + location.hostname;
|
||||||
|
|
||||||
document.cookie = 'PREFS=' + cookieData + '; SameSite=Strict; path=/; domain=' +
|
document.cookie = 'PREFS=' + cookieData + '; SameSite=Strict; path=/; domain=' +
|
||||||
domainUsed + '; expires=' + date.toGMTString() + ';';
|
domainUsed + '; expires=' + date.toGMTString() + ';';
|
||||||
|
@ -280,7 +282,7 @@ player.on('volumechange', function () {
|
||||||
|
|
||||||
player.on('waiting', function () {
|
player.on('waiting', function () {
|
||||||
if (player.playbackRate() > 1 && player.liveTracker.isLive() && player.liveTracker.atLiveEdge()) {
|
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);
|
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) {
|
if (video_data.params.save_player_pos) {
|
||||||
const url = new URL(location);
|
const url = new URL(location);
|
||||||
const hasTimeParam = url.searchParams.has('t');
|
const hasTimeParam = url.searchParams.has('t');
|
||||||
const remeberedTime = get_video_time();
|
const rememberedTime = get_video_time();
|
||||||
let lastUpdated = 0;
|
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 raw = player.currentTime();
|
||||||
const time = Math.floor(raw);
|
const time = Math.floor(raw);
|
||||||
|
|
||||||
|
@ -305,9 +307,7 @@ if (video_data.params.save_player_pos) {
|
||||||
save_video_time(time);
|
save_video_time(time);
|
||||||
lastUpdated = time;
|
lastUpdated = time;
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
player.on('timeupdate', updateTime);
|
|
||||||
}
|
}
|
||||||
else remove_all_video_times();
|
else remove_all_video_times();
|
||||||
|
|
||||||
|
@ -347,53 +347,31 @@ if (!video_data.params.listen && video_data.params.quality === 'dash') {
|
||||||
targetQualityLevel = 0;
|
targetQualityLevel = 0;
|
||||||
break;
|
break;
|
||||||
default:
|
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++) {
|
for (let i = 0; i < qualityLevels.length; i++) {
|
||||||
if (qualityLevels[i].height <= targetHeight) {
|
if (qualityLevels[i].height <= targetHeight)
|
||||||
targetQualityLevel = i;
|
targetQualityLevel = i;
|
||||||
} else {
|
else
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (let i = 0; i < qualityLevels.length; i++) {
|
qualityLevels.forEach(function (level, index) {
|
||||||
qualityLevels[i].enabled = (i === targetQualityLevel);
|
level.enabled = (index === targetQualityLevel);
|
||||||
}
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
player.vttThumbnails({
|
player.vttThumbnails({
|
||||||
src: location.origin + '/api/v1/storyboards/' + video_data.id + '?height=90',
|
src: '/api/v1/storyboards/' + video_data.id + '?height=90',
|
||||||
showTimestamp: true
|
showTimestamp: true
|
||||||
});
|
});
|
||||||
|
|
||||||
// Enable annotations
|
// Enable annotations
|
||||||
if (!video_data.params.listen && video_data.params.annotations) {
|
if (!video_data.params.listen && video_data.params.annotations) {
|
||||||
window.addEventListener('load', function (e) {
|
addEventListener('load', function (e) {
|
||||||
var video_container = document.getElementById('player');
|
addEventListener('__ar_annotation_click', function (e) {
|
||||||
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) {
|
|
||||||
const url = e.detail.url,
|
const url = e.detail.url,
|
||||||
target = e.detail.target,
|
target = e.detail.target,
|
||||||
seconds = e.detail.seconds;
|
seconds = e.detail.seconds;
|
||||||
|
@ -406,41 +384,48 @@ if (!video_data.params.listen && video_data.params.annotations) {
|
||||||
path = path.pathname + path.search;
|
path = path.pathname + path.search;
|
||||||
|
|
||||||
if (target === 'current') {
|
if (target === 'current') {
|
||||||
window.location.href = path;
|
location.href = path;
|
||||||
} else if (target === 'new') {
|
} 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();
|
const curVolume = player.volume();
|
||||||
let newVolume = curVolume + delta;
|
let newVolume = curVolume + delta;
|
||||||
if (newVolume > 1) {
|
newVolume = helpers.clamp(newVolume, 0, 1);
|
||||||
newVolume = 1;
|
|
||||||
} else if (newVolume < 0) {
|
|
||||||
newVolume = 0;
|
|
||||||
}
|
|
||||||
player.volume(newVolume);
|
player.volume(newVolume);
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggle_muted() {
|
function toggle_muted() {
|
||||||
const isMuted = player.muted();
|
player.muted(!player.muted());
|
||||||
player.muted(!isMuted);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function skip_seconds(delta) {
|
function skip_seconds(delta) {
|
||||||
const duration = player.duration();
|
const duration = player.duration();
|
||||||
const curTime = player.currentTime();
|
const curTime = player.currentTime();
|
||||||
let newTime = curTime + delta;
|
let newTime = curTime + delta;
|
||||||
if (newTime > duration) {
|
newTime = helpers.clamp(newTime, 0, duration);
|
||||||
newTime = duration;
|
|
||||||
} else if (newTime < 0) {
|
|
||||||
newTime = 0;
|
|
||||||
}
|
|
||||||
player.currentTime(newTime);
|
player.currentTime(newTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -450,57 +435,21 @@ function set_seconds_after_start(delta) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function save_video_time(seconds) {
|
function save_video_time(seconds) {
|
||||||
const videoId = video_data.id;
|
|
||||||
const all_video_times = get_all_video_times();
|
const all_video_times = get_all_video_times();
|
||||||
|
all_video_times[video_data.id] = seconds;
|
||||||
all_video_times[videoId] = seconds;
|
helpers.storage.set(save_player_pos_key, all_video_times);
|
||||||
|
|
||||||
set_all_video_times(all_video_times);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function get_video_time() {
|
function get_video_time() {
|
||||||
try {
|
return get_all_video_times()[video_data.id] || 0;
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function get_all_video_times() {
|
function get_all_video_times() {
|
||||||
if (storage) {
|
return helpers.storage.get(save_player_pos_key) || {};
|
||||||
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 {};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function remove_all_video_times() {
|
function remove_all_video_times() {
|
||||||
set_all_video_times(null);
|
helpers.storage.remove(save_player_pos_key);
|
||||||
}
|
}
|
||||||
|
|
||||||
function set_time_percent(percent) {
|
function set_time_percent(percent) {
|
||||||
|
@ -516,21 +465,23 @@ function toggle_play() { player.paused() ? play() : pause(); }
|
||||||
|
|
||||||
const toggle_captions = (function () {
|
const toggle_captions = (function () {
|
||||||
let toggledTrack = null;
|
let toggledTrack = null;
|
||||||
const onChange = function (e) {
|
|
||||||
toggledTrack = null;
|
function bindChange(onOrOff) {
|
||||||
};
|
player.textTracks()[onOrOff]('change', function (e) {
|
||||||
const bindChange = function (onOrOff) {
|
toggledTrack = null;
|
||||||
player.textTracks()[onOrOff]('change', onChange);
|
});
|
||||||
};
|
}
|
||||||
|
|
||||||
// Wrapper function to ignore our own emitted events and only listen
|
// Wrapper function to ignore our own emitted events and only listen
|
||||||
// to events emitted by Video.js on click on the captions menu items.
|
// to events emitted by Video.js on click on the captions menu items.
|
||||||
const setMode = function (track, mode) {
|
function setMode(track, mode) {
|
||||||
bindChange('off');
|
bindChange('off');
|
||||||
track.mode = mode;
|
track.mode = mode;
|
||||||
window.setTimeout(function () {
|
setTimeout(function () {
|
||||||
bindChange('on');
|
bindChange('on');
|
||||||
}, 0);
|
}, 0);
|
||||||
};
|
}
|
||||||
|
|
||||||
bindChange('on');
|
bindChange('on');
|
||||||
return function () {
|
return function () {
|
||||||
if (toggledTrack !== null) {
|
if (toggledTrack !== null) {
|
||||||
|
@ -578,15 +529,11 @@ function increase_playback_rate(steps) {
|
||||||
const maxIndex = options.playbackRates.length - 1;
|
const maxIndex = options.playbackRates.length - 1;
|
||||||
const curIndex = options.playbackRates.indexOf(player.playbackRate());
|
const curIndex = options.playbackRates.indexOf(player.playbackRate());
|
||||||
let newIndex = curIndex + steps;
|
let newIndex = curIndex + steps;
|
||||||
if (newIndex > maxIndex) {
|
newIndex = helpers.clamp(newIndex, 0, maxIndex);
|
||||||
newIndex = maxIndex;
|
|
||||||
} else if (newIndex < 0) {
|
|
||||||
newIndex = 0;
|
|
||||||
}
|
|
||||||
player.playbackRate(options.playbackRates[newIndex]);
|
player.playbackRate(options.playbackRates[newIndex]);
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('keydown', function (e) {
|
addEventListener('keydown', function (e) {
|
||||||
if (e.target.tagName.toLowerCase() === 'input') {
|
if (e.target.tagName.toLowerCase() === 'input') {
|
||||||
// Ignore input when focus is on certain elements, e.g. form fields.
|
// Ignore input when focus is on certain elements, e.g. form fields.
|
||||||
return;
|
return;
|
||||||
|
@ -619,10 +566,10 @@ window.addEventListener('keydown', function (e) {
|
||||||
case 'MediaStop': action = stop; break;
|
case 'MediaStop': action = stop; break;
|
||||||
|
|
||||||
case 'ArrowUp':
|
case 'ArrowUp':
|
||||||
if (isPlayerFocused) action = increase_volume.bind(this, 0.1);
|
if (isPlayerFocused) action = change_volume.bind(this, 0.1);
|
||||||
break;
|
break;
|
||||||
case 'ArrowDown':
|
case 'ArrowDown':
|
||||||
if (isPlayerFocused) action = increase_volume.bind(this, -0.1);
|
if (isPlayerFocused) action = change_volume.bind(this, -0.1);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'm':
|
case 'm':
|
||||||
|
@ -673,12 +620,11 @@ window.addEventListener('keydown', function (e) {
|
||||||
// TODO: Add support to play back previous video.
|
// TODO: Add support to play back previous video.
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case '.':
|
// TODO: More precise step. Now FPS is taken equal to 29.97
|
||||||
// TODO: Add support for next-frame-stepping.
|
// Common FPS: https://forum.videohelp.com/threads/81868#post323588
|
||||||
break;
|
// Possible solution is new HTMLVideoElement.requestVideoFrameCallback() https://wicg.github.io/video-rvfc/
|
||||||
case ',':
|
case ',': action = function () { pause(); skip_seconds(-1/29.97); }; break;
|
||||||
// TODO: Add support for previous-frame-stepping.
|
case '.': action = function () { pause(); skip_seconds( 1/29.97); }; break;
|
||||||
break;
|
|
||||||
|
|
||||||
case '>': action = increase_playback_rate.bind(this, 1); break;
|
case '>': action = increase_playback_rate.bind(this, 1); 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
|
// 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
|
// https://github.com/ctd1500/videojs-hotkeys/blob/bb4a158b2e214ccab87c2e7b95f42bc45c6bfd87/videojs.hotkeys.js#L292-L328
|
||||||
(function () {
|
(function () {
|
||||||
const volumeStep = 0.05;
|
|
||||||
const enableVolumeScroll = true;
|
|
||||||
const enableHoverScroll = true;
|
|
||||||
const doc = document;
|
|
||||||
const pEl = document.getElementById('player');
|
const pEl = document.getElementById('player');
|
||||||
|
|
||||||
var volumeHover = false;
|
var volumeHover = false;
|
||||||
|
@ -710,39 +652,23 @@ window.addEventListener('keydown', function (e) {
|
||||||
volumeSelector.onmouseout = function () { volumeHover = false; };
|
volumeSelector.onmouseout = function () { volumeHover = false; };
|
||||||
}
|
}
|
||||||
|
|
||||||
var mouseScroll = function mouseScroll(event) {
|
function mouseScroll(event) {
|
||||||
var activeEl = doc.activeElement;
|
|
||||||
if (enableHoverScroll) {
|
|
||||||
// If we leave this undefined then it can match non-existent elements below
|
|
||||||
activeEl = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// When controls are disabled, hotkeys will be disabled as well
|
// When controls are disabled, hotkeys will be disabled as well
|
||||||
if (player.controls()) {
|
if (!player.controls() || !volumeHover) return;
|
||||||
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) {
|
event.preventDefault();
|
||||||
increase_volume(volumeStep);
|
var wheelMove = event.wheelDelta || -event.detail;
|
||||||
} else if (delta === -1) {
|
var volumeSign = Math.sign(wheelMove);
|
||||||
increase_volume(-volumeStep);
|
|
||||||
}
|
change_volume(volumeSign * 0.05); // decrease/increase by 5%
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
player.on('mousewheel', mouseScroll);
|
player.on('mousewheel', mouseScroll);
|
||||||
player.on('DOMMouseScroll', mouseScroll);
|
player.on('DOMMouseScroll', mouseScroll);
|
||||||
}());
|
}());
|
||||||
|
|
||||||
// Since videojs-share can sometimes be blocked, we defer it until last
|
// Since videojs-share can sometimes be blocked, we defer it until last
|
||||||
if (player.share) {
|
if (player.share) player.share(shareOptions);
|
||||||
player.share(shareOptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
// show the preferred caption by default
|
// show the preferred caption by default
|
||||||
if (player_data.preferred_caption_found) {
|
if (player_data.preferred_caption_found) {
|
||||||
|
@ -763,7 +689,7 @@ if (navigator.vendor === 'Apple Computer, Inc.' && video_data.params.listen) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Watch on Invidious link
|
// Watch on Invidious link
|
||||||
if (window.location.pathname.startsWith('/embed/')) {
|
if (location.pathname.startsWith('/embed/')) {
|
||||||
const Button = videojs.getComponent('Button');
|
const Button = videojs.getComponent('Button');
|
||||||
let watch_on_invidious_button = new Button(player);
|
let watch_on_invidious_button = new Button(player);
|
||||||
|
|
||||||
|
@ -778,3 +704,11 @@ if (window.location.pathname.startsWith('/embed/')) {
|
||||||
var cb = player.getChild('ControlBar');
|
var cb = player.getChild('ControlBar');
|
||||||
cb.addChild(watch_on_invidious_button);
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
var playlist_data = JSON.parse(document.getElementById('playlist_data').textContent);
|
var playlist_data = JSON.parse(document.getElementById('playlist_data').textContent);
|
||||||
|
var payload = 'csrf_token=' + playlist_data.csrf_token;
|
||||||
|
|
||||||
function add_playlist_video(target) {
|
function add_playlist_video(target) {
|
||||||
var select = target.parentNode.children[0].children[1];
|
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' +
|
var url = '/playlist_ajax?action_add_video=1&redirect=false' +
|
||||||
'&video_id=' + target.getAttribute('data-id') +
|
'&video_id=' + target.getAttribute('data-id') +
|
||||||
'&playlist_id=' + option.getAttribute('data-plid');
|
'&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 () {
|
helpers.xhr('POST', url, {payload: payload}, {
|
||||||
if (xhr.readyState === 4) {
|
on200: function (response) {
|
||||||
if (xhr.status === 200) {
|
option.textContent = '✓' + option.textContent;
|
||||||
option.innerText = '✓' + option.innerText;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
xhr.send('csrf_token=' + playlist_data.csrf_token);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function add_playlist_item(target) {
|
function add_playlist_item(target) {
|
||||||
|
@ -32,21 +24,12 @@ function add_playlist_item(target) {
|
||||||
var url = '/playlist_ajax?action_add_video=1&redirect=false' +
|
var url = '/playlist_ajax?action_add_video=1&redirect=false' +
|
||||||
'&video_id=' + target.getAttribute('data-id') +
|
'&video_id=' + target.getAttribute('data-id') +
|
||||||
'&playlist_id=' + target.getAttribute('data-plid');
|
'&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 () {
|
helpers.xhr('POST', url, {payload: payload}, {
|
||||||
if (xhr.readyState === 4) {
|
onNon200: function (xhr) {
|
||||||
if (xhr.status !== 200) {
|
tile.style.display = '';
|
||||||
tile.style.display = '';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
xhr.send('csrf_token=' + playlist_data.csrf_token);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function remove_playlist_item(target) {
|
function remove_playlist_item(target) {
|
||||||
|
@ -56,19 +39,10 @@ function remove_playlist_item(target) {
|
||||||
var url = '/playlist_ajax?action_remove_video=1&redirect=false' +
|
var url = '/playlist_ajax?action_remove_video=1&redirect=false' +
|
||||||
'&set_video_id=' + target.getAttribute('data-index') +
|
'&set_video_id=' + target.getAttribute('data-index') +
|
||||||
'&playlist_id=' + target.getAttribute('data-plid');
|
'&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 () {
|
helpers.xhr('POST', url, {payload: payload}, {
|
||||||
if (xhr.readyState === 4) {
|
onNon200: function (xhr) {
|
||||||
if (xhr.status !== 200) {
|
tile.style.display = '';
|
||||||
tile.style.display = '';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
xhr.send('csrf_token=' + playlist_data.csrf_token);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
var subscribe_data = JSON.parse(document.getElementById('subscribe_data').textContent);
|
var subscribe_data = JSON.parse(document.getElementById('subscribe_data').textContent);
|
||||||
|
var payload = 'csrf_token=' + subscribe_data.csrf_token;
|
||||||
|
|
||||||
var subscribe_button = document.getElementById('subscribe');
|
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') {
|
if (subscribe_button.getAttribute('data-type') === 'subscribe') {
|
||||||
subscribe_button.onclick = subscribe;
|
subscribe_button.onclick = subscribe;
|
||||||
|
@ -10,87 +11,34 @@ if (subscribe_button.getAttribute('data-type') === 'subscribe') {
|
||||||
subscribe_button.onclick = unsubscribe;
|
subscribe_button.onclick = unsubscribe;
|
||||||
}
|
}
|
||||||
|
|
||||||
function subscribe(retries) {
|
function subscribe() {
|
||||||
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');
|
|
||||||
|
|
||||||
var fallback = subscribe_button.innerHTML;
|
var fallback = subscribe_button.innerHTML;
|
||||||
subscribe_button.onclick = unsubscribe;
|
subscribe_button.onclick = unsubscribe;
|
||||||
subscribe_button.innerHTML = '<b>' + subscribe_data.unsubscribe_text + ' | ' + subscribe_data.sub_count_text + '</b>';
|
subscribe_button.innerHTML = '<b>' + subscribe_data.unsubscribe_text + ' | ' + subscribe_data.sub_count_text + '</b>';
|
||||||
|
|
||||||
xhr.onreadystatechange = function () {
|
var url = '/subscription_ajax?action_create_subscription_to_channel=1&redirect=false' +
|
||||||
if (xhr.readyState === 4) {
|
'&c=' + subscribe_data.ucid;
|
||||||
if (xhr.status !== 200) {
|
|
||||||
subscribe_button.onclick = subscribe;
|
helpers.xhr('POST', url, {payload: payload, retries: 5, entity_name: 'subscribe request'}, {
|
||||||
subscribe_button.innerHTML = fallback;
|
onNon200: function (xhr) {
|
||||||
}
|
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);
|
|
||||||
};
|
|
||||||
|
|
||||||
xhr.send('csrf_token=' + subscribe_data.csrf_token);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function unsubscribe(retries) {
|
function unsubscribe() {
|
||||||
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');
|
|
||||||
|
|
||||||
var fallback = subscribe_button.innerHTML;
|
var fallback = subscribe_button.innerHTML;
|
||||||
subscribe_button.onclick = subscribe;
|
subscribe_button.onclick = subscribe;
|
||||||
subscribe_button.innerHTML = '<b>' + subscribe_data.subscribe_text + ' | ' + subscribe_data.sub_count_text + '</b>';
|
subscribe_button.innerHTML = '<b>' + subscribe_data.subscribe_text + ' | ' + subscribe_data.sub_count_text + '</b>';
|
||||||
|
|
||||||
xhr.onreadystatechange = function () {
|
var url = '/subscription_ajax?action_remove_subscriptions=1&redirect=false' +
|
||||||
if (xhr.readyState === 4) {
|
'&c=' + subscribe_data.ucid;
|
||||||
if (xhr.status !== 200) {
|
|
||||||
subscribe_button.onclick = unsubscribe;
|
helpers.xhr('POST', url, {payload: payload, retries: 5, entity_name: 'unsubscribe request'}, {
|
||||||
subscribe_button.innerHTML = fallback;
|
onNon200: function (xhr) {
|
||||||
}
|
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);
|
|
||||||
};
|
|
||||||
|
|
||||||
xhr.send('csrf_token=' + subscribe_data.csrf_token);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,90 +1,44 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
var toggle_theme = document.getElementById('toggle_theme');
|
var toggle_theme = document.getElementById('toggle_theme');
|
||||||
toggle_theme.href = 'javascript:void(0);';
|
toggle_theme.href = 'javascript:void(0)';
|
||||||
|
|
||||||
|
const STORAGE_KEY_THEME = 'dark_mode';
|
||||||
|
const THEME_DARK = 'dark';
|
||||||
|
const THEME_LIGHT = 'light';
|
||||||
|
|
||||||
|
// TODO: theme state controlled by system
|
||||||
toggle_theme.addEventListener('click', function () {
|
toggle_theme.addEventListener('click', function () {
|
||||||
var dark_mode = document.body.classList.contains('light-theme');
|
const isDarkTheme = helpers.storage.get(STORAGE_KEY_THEME) === THEME_DARK;
|
||||||
|
const newTheme = isDarkTheme ? THEME_LIGHT : THEME_DARK;
|
||||||
var url = '/toggle_theme?redirect=false';
|
setTheme(newTheme);
|
||||||
var xhr = new XMLHttpRequest();
|
helpers.storage.set(STORAGE_KEY_THEME, newTheme);
|
||||||
xhr.responseType = 'json';
|
helpers.xhr('GET', '/toggle_theme?redirect=false', {}, {});
|
||||||
xhr.timeout = 10000;
|
|
||||||
xhr.open('GET', url, true);
|
|
||||||
|
|
||||||
set_mode(dark_mode);
|
|
||||||
try {
|
|
||||||
window.localStorage.setItem('dark_mode', dark_mode ? 'dark' : 'light');
|
|
||||||
} catch (e) {}
|
|
||||||
|
|
||||||
xhr.send();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener('storage', function (e) {
|
/** @param {THEME_DARK|THEME_LIGHT} theme */
|
||||||
if (e.key === 'dark_mode') {
|
function setTheme(theme) {
|
||||||
update_mode(e.newValue);
|
// 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';
|
||||||
window.addEventListener('DOMContentLoaded', function () {
|
document.body.className = 'dark-theme';
|
||||||
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);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
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');
|
|
||||||
} else {
|
} else {
|
||||||
// light
|
toggle_theme.children[0].className = 'icon ion-ios-moon';
|
||||||
toggle_theme.children[0].setAttribute('class', 'icon ion-ios-moon');
|
document.body.className = 'light-theme';
|
||||||
document.body.classList.remove('no-theme');
|
|
||||||
document.body.classList.remove('dark-theme');
|
|
||||||
document.body.classList.add('light-theme');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function update_mode (mode) {
|
// Handles theme change event caused by other tab
|
||||||
if (mode === 'true' /* for backwards compatibility */ || mode === 'dark') {
|
addEventListener('storage', function (e) {
|
||||||
// If preference for dark mode indicated
|
if (e.key === STORAGE_KEY_THEME)
|
||||||
set_mode(true);
|
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 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);
|
|
||||||
}
|
|
||||||
// else do nothing, falling back to the mode defined by the `dark_mode` preference on the preferences page (backend)
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
var video_data = JSON.parse(document.getElementById('video_data').textContent);
|
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) {
|
String.prototype.supplant = function (o) {
|
||||||
return this.replace(/{([^{}]*)}/g, function (a, b) {
|
return this.replace(/{([^{}]*)}/g, function (a, b) {
|
||||||
|
@ -10,24 +12,24 @@ String.prototype.supplant = function (o) {
|
||||||
|
|
||||||
function toggle_parent(target) {
|
function toggle_parent(target) {
|
||||||
var body = target.parentNode.parentNode.children[1];
|
var body = target.parentNode.parentNode.children[1];
|
||||||
if (body.style.display === null || body.style.display === '') {
|
if (body.style.display === 'none') {
|
||||||
target.textContent = '[ + ]';
|
|
||||||
body.style.display = 'none';
|
|
||||||
} else {
|
|
||||||
target.textContent = '[ − ]';
|
target.textContent = '[ − ]';
|
||||||
body.style.display = '';
|
body.style.display = '';
|
||||||
|
} else {
|
||||||
|
target.textContent = '[ + ]';
|
||||||
|
body.style.display = 'none';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggle_comments(event) {
|
function toggle_comments(event) {
|
||||||
var target = event.target;
|
var target = event.target;
|
||||||
var body = target.parentNode.parentNode.parentNode.children[1];
|
var body = target.parentNode.parentNode.parentNode.children[1];
|
||||||
if (body.style.display === null || body.style.display === '') {
|
if (body.style.display === 'none') {
|
||||||
target.textContent = '[ + ]';
|
|
||||||
body.style.display = 'none';
|
|
||||||
} else {
|
|
||||||
target.textContent = '[ − ]';
|
target.textContent = '[ − ]';
|
||||||
body.style.display = '';
|
body.style.display = '';
|
||||||
|
} else {
|
||||||
|
target.textContent = '[ + ]';
|
||||||
|
body.style.display = 'none';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,56 +81,31 @@ if (continue_button) {
|
||||||
function next_video() {
|
function next_video() {
|
||||||
var url = new URL('https://example.com/watch?v=' + video_data.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');
|
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);
|
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);
|
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('local', video_data.params.local);
|
||||||
}
|
|
||||||
|
|
||||||
url.searchParams.set('continue', '1');
|
url.searchParams.set('continue', '1');
|
||||||
|
|
||||||
location.assign(url.pathname + url.search);
|
location.assign(url.pathname + url.search);
|
||||||
}
|
}
|
||||||
|
|
||||||
function continue_autoplay(event) {
|
function continue_autoplay(event) {
|
||||||
if (event.target.checked) {
|
if (event.target.checked) {
|
||||||
player.on('ended', function () {
|
player.on('ended', next_video);
|
||||||
next_video();
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
player.off('ended');
|
player.off('ended');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function number_with_separator(val) {
|
function get_playlist(plid) {
|
||||||
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;
|
|
||||||
var playlist = document.getElementById('playlist');
|
var playlist = document.getElementById('playlist');
|
||||||
|
|
||||||
if (retries <= 0) {
|
playlist.innerHTML = spinnerHTMLwithHR;
|
||||||
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>';
|
|
||||||
|
|
||||||
var plid_url;
|
var plid_url;
|
||||||
if (plid.startsWith('RD')) {
|
if (plid.startsWith('RD')) {
|
||||||
|
@ -142,225 +119,148 @@ function get_playlist(plid, retries) {
|
||||||
'&format=html&hl=' + video_data.preferences.locale;
|
'&format=html&hl=' + video_data.preferences.locale;
|
||||||
}
|
}
|
||||||
|
|
||||||
var xhr = new XMLHttpRequest();
|
helpers.xhr('GET', plid_url, {retries: 5, entity_name: 'playlist'}, {
|
||||||
xhr.responseType = 'json';
|
on200: function (response) {
|
||||||
xhr.timeout = 10000;
|
playlist.innerHTML = response.playlistHtml;
|
||||||
xhr.open('GET', plid_url, true);
|
|
||||||
|
|
||||||
xhr.onreadystatechange = function () {
|
if (!response.nextVideo) return;
|
||||||
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) {
|
var nextVideo = document.getElementById(response.nextVideo);
|
||||||
player.on('ended', function () {
|
nextVideo.parentNode.parentNode.scrollTop = nextVideo.offsetTop;
|
||||||
var url = new URL('https://example.com/watch?v=' + xhr.response.nextVideo);
|
|
||||||
|
|
||||||
url.searchParams.set('list', plid);
|
player.on('ended', function () {
|
||||||
if (!plid.startsWith('RD')) {
|
var url = new URL('https://example.com/watch?v=' + response.nextVideo);
|
||||||
url.searchParams.set('index', xhr.response.index);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (video_data.params.autoplay || video_data.params.continue_autoplay) {
|
url.searchParams.set('list', plid);
|
||||||
url.searchParams.set('autoplay', '1');
|
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);
|
||||||
|
|
||||||
if (video_data.params.listen !== video_data.preferences.listen) {
|
location.assign(url.pathname + url.search);
|
||||||
url.searchParams.set('listen', video_data.params.listen);
|
});
|
||||||
}
|
},
|
||||||
|
onNon200: function (xhr) {
|
||||||
if (video_data.params.speed !== video_data.preferences.speed) {
|
playlist.innerHTML = '';
|
||||||
url.searchParams.set('speed', video_data.params.speed);
|
document.getElementById('continue').style.display = '';
|
||||||
}
|
},
|
||||||
|
onError: function (xhr) {
|
||||||
if (video_data.params.local !== video_data.preferences.local) {
|
playlist.innerHTML = spinnerHTMLwithHR;
|
||||||
url.searchParams.set('local', video_data.params.local);
|
},
|
||||||
}
|
onTimeout: function (xhr) {
|
||||||
|
playlist.innerHTML = spinnerHTMLwithHR;
|
||||||
location.assign(url.pathname + url.search);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
playlist.innerHTML = '';
|
|
||||||
document.getElementById('continue').style.display = '';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
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) {
|
function get_reddit_comments() {
|
||||||
if (retries === undefined) retries = 5;
|
|
||||||
var comments = document.getElementById('comments');
|
var comments = document.getElementById('comments');
|
||||||
|
|
||||||
if (retries <= 0) {
|
|
||||||
console.warn('Failed to pull comments');
|
|
||||||
comments.innerHTML = '';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var fallback = comments.innerHTML;
|
var fallback = comments.innerHTML;
|
||||||
comments.innerHTML =
|
comments.innerHTML = spinnerHTML;
|
||||||
'<h3 style="text-align:center"><div class="loading"><i class="icon ion-ios-refresh"></i></div></h3>';
|
|
||||||
|
|
||||||
var url = '/api/v1/comments/' + video_data.id +
|
var url = '/api/v1/comments/' + video_data.id +
|
||||||
'?source=reddit&format=html' +
|
'?source=reddit&format=html' +
|
||||||
'&hl=' + video_data.preferences.locale;
|
'&hl=' + video_data.preferences.locale;
|
||||||
var xhr = new XMLHttpRequest();
|
|
||||||
xhr.responseType = 'json';
|
|
||||||
xhr.timeout = 10000;
|
|
||||||
xhr.open('GET', url, true);
|
|
||||||
|
|
||||||
xhr.onreadystatechange = function () {
|
var onNon200 = function (xhr) { comments.innerHTML = fallback; };
|
||||||
if (xhr.readyState === 4) {
|
if (video_data.params.comments[1] === 'youtube')
|
||||||
if (xhr.status === 200) {
|
onNon200 = function (xhr) {};
|
||||||
comments.innerHTML = ' \
|
|
||||||
<div> \
|
helpers.xhr('GET', url, {retries: 5, entity_name: ''}, {
|
||||||
<h3> \
|
on200: function (response) {
|
||||||
<a href="javascript:void(0)">[ − ]</a> \
|
comments.innerHTML = ' \
|
||||||
{title} \
|
<div> \
|
||||||
</h3> \
|
<h3> \
|
||||||
<p> \
|
<a href="javascript:void(0)">[ − ]</a> \
|
||||||
<b> \
|
{title} \
|
||||||
<a href="javascript:void(0)" data-comments="youtube"> \
|
</h3> \
|
||||||
{youtubeCommentsText} \
|
<p> \
|
||||||
</a> \
|
|
||||||
</b> \
|
|
||||||
</p> \
|
|
||||||
<b> \
|
<b> \
|
||||||
<a rel="noopener" target="_blank" href="https://reddit.com{permalink}">{redditPermalinkText}</a> \
|
<a href="javascript:void(0)" data-comments="youtube"> \
|
||||||
|
{youtubeCommentsText} \
|
||||||
|
</a> \
|
||||||
</b> \
|
</b> \
|
||||||
</div> \
|
</p> \
|
||||||
<div>{contentHtml}</div> \
|
<b> \
|
||||||
<hr>'.supplant({
|
<a rel="noopener" target="_blank" href="https://reddit.com{permalink}">{redditPermalinkText}</a> \
|
||||||
title: xhr.response.title,
|
</b> \
|
||||||
youtubeCommentsText: video_data.youtube_comments_text,
|
</div> \
|
||||||
redditPermalinkText: video_data.reddit_permalink_text,
|
<div>{contentHtml}</div> \
|
||||||
permalink: xhr.response.permalink,
|
<hr>'.supplant({
|
||||||
contentHtml: xhr.response.contentHtml
|
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[0].children[0].onclick = toggle_comments;
|
||||||
comments.children[0].children[1].children[0].onclick = swap_comments;
|
comments.children[0].children[1].children[0].onclick = swap_comments;
|
||||||
} else {
|
},
|
||||||
if (video_data.params.comments[1] === 'youtube') {
|
onNon200: onNon200, // declared above
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function get_youtube_comments(retries) {
|
function get_youtube_comments() {
|
||||||
if (retries === undefined) retries = 5;
|
|
||||||
var comments = document.getElementById('comments');
|
var comments = document.getElementById('comments');
|
||||||
|
|
||||||
if (retries <= 0) {
|
|
||||||
console.warn('Failed to pull comments');
|
|
||||||
comments.innerHTML = '';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var fallback = comments.innerHTML;
|
var fallback = comments.innerHTML;
|
||||||
comments.innerHTML =
|
comments.innerHTML = spinnerHTML;
|
||||||
'<h3 style="text-align:center"><div class="loading"><i class="icon ion-ios-refresh"></i></div></h3>';
|
|
||||||
|
|
||||||
var url = '/api/v1/comments/' + video_data.id +
|
var url = '/api/v1/comments/' + video_data.id +
|
||||||
'?format=html' +
|
'?format=html' +
|
||||||
'&hl=' + video_data.preferences.locale +
|
'&hl=' + video_data.preferences.locale +
|
||||||
'&thin_mode=' + video_data.preferences.thin_mode;
|
'&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 () {
|
var onNon200 = function (xhr) { comments.innerHTML = fallback; };
|
||||||
if (xhr.readyState === 4) {
|
if (video_data.params.comments[1] === 'youtube')
|
||||||
if (xhr.status === 200) {
|
onNon200 = function (xhr) {};
|
||||||
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;
|
helpers.xhr('GET', url, {retries: 5, entity_name: 'comments'}, {
|
||||||
comments.children[0].children[1].children[0].onclick = swap_comments;
|
on200: function (response) {
|
||||||
} else {
|
comments.innerHTML = ' \
|
||||||
if (video_data.params.comments[1] === 'youtube') {
|
<div> \
|
||||||
setTimeout(function () { get_youtube_comments(retries - 1); }, 1000);
|
<h3> \
|
||||||
} else {
|
<a href="javascript:void(0)">[ − ]</a> \
|
||||||
comments.innerHTML = '';
|
{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) {
|
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 body = target.parentNode.parentNode;
|
||||||
var fallback = body.innerHTML;
|
var fallback = body.innerHTML;
|
||||||
body.innerHTML =
|
body.innerHTML = spinnerHTML;
|
||||||
'<h3 style="text-align:center"><div class="loading"><i class="icon ion-ios-refresh"></i></div></h3>';
|
|
||||||
|
|
||||||
var url = '/api/v1/comments/' + video_data.id +
|
var url = '/api/v1/comments/' + video_data.id +
|
||||||
'?format=html' +
|
'?format=html' +
|
||||||
'&hl=' + video_data.preferences.locale +
|
'&hl=' + video_data.preferences.locale +
|
||||||
'&thin_mode=' + video_data.preferences.thin_mode +
|
'&thin_mode=' + video_data.preferences.thin_mode +
|
||||||
'&continuation=' + continuation;
|
'&continuation=' + continuation;
|
||||||
if (load_replies) {
|
if (load_replies) url += '&action=action_get_comment_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 () {
|
helpers.xhr('GET', url, {}, {
|
||||||
if (xhr.readyState === 4) {
|
on200: function (response) {
|
||||||
if (xhr.status === 200) {
|
if (load_more) {
|
||||||
if (load_more) {
|
body = body.parentNode.parentNode;
|
||||||
body = body.parentNode.parentNode;
|
body.removeChild(body.lastElementChild);
|
||||||
body.removeChild(body.lastElementChild);
|
body.innerHTML += response.contentHtml;
|
||||||
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);
|
|
||||||
}
|
|
||||||
} else {
|
} 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;
|
||||||
|
|
||||||
|
var div = document.createElement('div');
|
||||||
|
div.innerHTML = response.contentHtml;
|
||||||
|
|
||||||
|
body.appendChild(p);
|
||||||
|
body.appendChild(div);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
onNon200: function (xhr) {
|
||||||
|
body.innerHTML = fallback;
|
||||||
|
},
|
||||||
|
onTimeout: function (xhr) {
|
||||||
|
console.warn('Pulling comments failed');
|
||||||
|
body.innerHTML = fallback;
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
xhr.ontimeout = function () {
|
|
||||||
console.warn('Pulling comments failed.');
|
|
||||||
body.innerHTML = fallback;
|
|
||||||
};
|
|
||||||
|
|
||||||
xhr.send();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (video_data.play_next) {
|
if (video_data.play_next) {
|
||||||
player.on('ended', function () {
|
player.on('ended', function () {
|
||||||
var url = new URL('https://example.com/watch?v=' + video_data.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');
|
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);
|
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);
|
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('local', video_data.params.local);
|
||||||
}
|
|
||||||
|
|
||||||
url.searchParams.set('continue', '1');
|
url.searchParams.set('continue', '1');
|
||||||
|
|
||||||
location.assign(url.pathname + url.search);
|
location.assign(url.pathname + url.search);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('load', function (e) {
|
addEventListener('load', function (e) {
|
||||||
if (video_data.plid) {
|
if (video_data.plid)
|
||||||
get_playlist(video_data.plid);
|
get_playlist(video_data.plid);
|
||||||
}
|
|
||||||
|
|
||||||
if (video_data.params.comments[0] === 'youtube') {
|
if (video_data.params.comments[0] === 'youtube') {
|
||||||
get_youtube_comments();
|
get_youtube_comments();
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
var watched_data = JSON.parse(document.getElementById('watched_data').textContent);
|
var watched_data = JSON.parse(document.getElementById('watched_data').textContent);
|
||||||
|
var payload = 'csrf_token=' + watched_data.csrf_token;
|
||||||
|
|
||||||
function mark_watched(target) {
|
function mark_watched(target) {
|
||||||
var tile = target.parentNode.parentNode.parentNode.parentNode.parentNode;
|
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' +
|
var url = '/watch_ajax?action_mark_watched=1&redirect=false' +
|
||||||
'&id=' + target.getAttribute('data-id');
|
'&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 () {
|
helpers.xhr('POST', url, {payload: payload}, {
|
||||||
if (xhr.readyState === 4) {
|
onNon200: function (xhr) {
|
||||||
if (xhr.status !== 200) {
|
tile.style.display = '';
|
||||||
tile.style.display = '';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
xhr.send('csrf_token=' + watched_data.csrf_token);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function mark_unwatched(target) {
|
function mark_unwatched(target) {
|
||||||
var tile = target.parentNode.parentNode.parentNode.parentNode.parentNode;
|
var tile = target.parentNode.parentNode.parentNode.parentNode.parentNode;
|
||||||
tile.style.display = 'none';
|
tile.style.display = 'none';
|
||||||
var count = document.getElementById('count');
|
var count = document.getElementById('count');
|
||||||
count.innerText = count.innerText - 1;
|
count.textContent--;
|
||||||
|
|
||||||
var url = '/watch_ajax?action_mark_unwatched=1&redirect=false' +
|
var url = '/watch_ajax?action_mark_unwatched=1&redirect=false' +
|
||||||
'&id=' + target.getAttribute('data-id');
|
'&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 () {
|
helpers.xhr('POST', url, {payload: payload}, {
|
||||||
if (xhr.readyState === 4) {
|
onNon200: function (xhr) {
|
||||||
if (xhr.status !== 200) {
|
count.textContent++;
|
||||||
count.innerText = count.innerText - 1 + 2;
|
tile.style.display = '';
|
||||||
tile.style.display = '';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
xhr.send('csrf_token=' + watched_data.csrf_token);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,97 @@
|
||||||
|
{
|
||||||
|
"Subscribe": "সাবস্ক্রাইব",
|
||||||
|
"View channel on YouTube": "ইউটিউবে চ্যানেল দেখুন",
|
||||||
|
"View playlist on YouTube": "ইউটিউবে প্লেলিস্ট দেখুন",
|
||||||
|
"newest": "সর্ব-নতুন",
|
||||||
|
"oldest": "পুরানতম",
|
||||||
|
"popular": "জনপ্রিয়",
|
||||||
|
"last": "শেষটা",
|
||||||
|
"Next page": "পরের পৃষ্ঠা",
|
||||||
|
"Previous page": "আগের পৃষ্ঠা",
|
||||||
|
"Clear watch history?": "দেখার ইতিহাস সাফ করবেন?",
|
||||||
|
"New password": "নতুন পাসওয়ার্ড",
|
||||||
|
"New passwords must match": "নতুন পাসওয়ার্ড অবশ্যই মিলতে হবে",
|
||||||
|
"Cannot change password for Google accounts": "গুগল অ্যাকাউন্টগুলোর জন্য পাসওয়ার্ড পরিবর্তন করা যায় না",
|
||||||
|
"Authorize token?": "টোকেন অনুমোদন করবেন?",
|
||||||
|
"Authorize token for `x`?": "`x` -এর জন্য টোকেন অনুমোদন?",
|
||||||
|
"Yes": "হ্যাঁ",
|
||||||
|
"No": "না",
|
||||||
|
"Import and Export Data": "তথ্য আমদানি ও রপ্তানি",
|
||||||
|
"Import": "আমদানি",
|
||||||
|
"Import Invidious data": "ইনভিডিয়াস তথ্য আমদানি",
|
||||||
|
"Import YouTube subscriptions": "ইউটিউব সাবস্ক্রিপশন আনুন",
|
||||||
|
"Import FreeTube subscriptions (.db)": "ফ্রিটিউব সাবস্ক্রিপশন (.db) আনুন",
|
||||||
|
"Import NewPipe subscriptions (.json)": "নতুন পাইপ সাবস্ক্রিপশন আনুন (.json)",
|
||||||
|
"Import NewPipe data (.zip)": "নিউপাইপ তথ্য আনুন (.zip)",
|
||||||
|
"Export": "তথ্য বের করুন",
|
||||||
|
"Export subscriptions as OPML": "সাবস্ক্রিপশন OPML হিসাবে আনুন",
|
||||||
|
"Export subscriptions as OPML (for NewPipe & FreeTube)": "OPML-এ সাবস্ক্রিপশন বের করুন(নিউ পাইপ এবং ফ্রিউটিউব এর জন্য)",
|
||||||
|
"Export data as JSON": "JSON হিসাবে তথ্য বের করুন",
|
||||||
|
"Delete account?": "অ্যাকাউন্ট মুছে ফেলবেন?",
|
||||||
|
"History": "ইতিহাস",
|
||||||
|
"An alternative front-end to YouTube": "ইউটিউবের একটি বিকল্পস্বরূপ সম্মুখ-প্রান্ত",
|
||||||
|
"JavaScript license information": "জাভাস্ক্রিপ্ট লাইসেন্সের তথ্য",
|
||||||
|
"source": "সূত্র",
|
||||||
|
"Log in": "লগ ইন",
|
||||||
|
"Log in/register": "লগ ইন/রেজিস্টার",
|
||||||
|
"Log in with Google": "গুগল দিয়ে লগ ইন করুন",
|
||||||
|
"User ID": "ইউজার আইডি",
|
||||||
|
"Password": "পাসওয়ার্ড",
|
||||||
|
"Time (h:mm:ss):": "সময় (ঘণ্টা:মিনিট:সেকেন্ড):",
|
||||||
|
"Text CAPTCHA": "টেক্সট ক্যাপচা",
|
||||||
|
"Image CAPTCHA": "চিত্র ক্যাপচা",
|
||||||
|
"Sign In": "সাইন ইন",
|
||||||
|
"Register": "নিবন্ধন",
|
||||||
|
"E-mail": "ই-মেইল",
|
||||||
|
"Google verification code": "গুগল যাচাইকরণ কোড",
|
||||||
|
"Preferences": "পছন্দসমূহ",
|
||||||
|
"preferences_category_player": "প্লেয়ারের পছন্দসমূহ",
|
||||||
|
"preferences_video_loop_label": "সর্বদা লুপ: ",
|
||||||
|
"preferences_autoplay_label": "স্বয়ংক্রিয় চালু: ",
|
||||||
|
"preferences_continue_label": "ডিফল্টভাবে পরবর্তী চালাও: ",
|
||||||
|
"preferences_continue_autoplay_label": "পরবর্তী ভিডিও স্বয়ংক্রিয়ভাবে চালাও: ",
|
||||||
|
"preferences_listen_label": "সহজাতভাবে শোনো: ",
|
||||||
|
"preferences_local_label": "ভিডিও প্রক্সি করো: ",
|
||||||
|
"preferences_speed_label": "সহজাত গতি: ",
|
||||||
|
"preferences_quality_label": "পছন্দের ভিডিও মান: ",
|
||||||
|
"preferences_volume_label": "প্লেয়ার শব্দের মাত্রা: ",
|
||||||
|
"LIVE": "লাইভ",
|
||||||
|
"Shared `x` ago": "`x` আগে শেয়ার করা হয়েছে",
|
||||||
|
"Unsubscribe": "আনসাবস্ক্রাইব",
|
||||||
|
"generic_views_count": "{{count}}জন দেখেছে",
|
||||||
|
"generic_views_count_plural": "{{count}}জন দেখেছে",
|
||||||
|
"generic_videos_count": "{{count}}টি ভিডিও",
|
||||||
|
"generic_videos_count_plural": "{{count}}টি ভিডিও",
|
||||||
|
"generic_subscribers_count": "{{count}}জন অনুসরণকারী",
|
||||||
|
"generic_subscribers_count_plural": "{{count}}জন অনুসরণকারী",
|
||||||
|
"preferences_watch_history_label": "দেখার ইতিহাস চালু করো: ",
|
||||||
|
"preferences_quality_option_dash": "ড্যাশ (সময়োপযোগী মান)",
|
||||||
|
"preferences_quality_dash_option_auto": "স্বয়ংক্রিয়",
|
||||||
|
"preferences_quality_dash_option_best": "সেরা",
|
||||||
|
"preferences_quality_dash_option_worst": "মন্দতম",
|
||||||
|
"preferences_quality_dash_option_4320p": "৪৩২০পি",
|
||||||
|
"preferences_quality_dash_option_2160p": "২১৬০পি",
|
||||||
|
"preferences_quality_dash_option_1440p": "১৪৪০পি",
|
||||||
|
"preferences_quality_dash_option_480p": "৪৮০পি",
|
||||||
|
"preferences_quality_dash_option_360p": "৩৬০পি",
|
||||||
|
"preferences_quality_dash_option_240p": "২৪০পি",
|
||||||
|
"preferences_quality_dash_option_144p": "১৪৪পি",
|
||||||
|
"preferences_comments_label": "সহজাত মন্তব্য: ",
|
||||||
|
"youtube": "ইউটিউব",
|
||||||
|
"Fallback captions: ": "বিকল্প উপাখ্যান: ",
|
||||||
|
"preferences_related_videos_label": "সম্পর্কিত ভিডিও দেখাও: ",
|
||||||
|
"preferences_annotations_label": "সহজাতভাবে টীকা দেখাও ",
|
||||||
|
"preferences_quality_option_hd720": "উচ্চ৭২০",
|
||||||
|
"preferences_quality_dash_label": "পছন্দের ড্যাশ ভিডিও মান: ",
|
||||||
|
"preferences_captions_label": "সহজাত উপাখ্যান: ",
|
||||||
|
"generic_playlists_count": "{{count}}টি চালুতালিকা",
|
||||||
|
"generic_playlists_count_plural": "{{count}}টি চালুতালিকা",
|
||||||
|
"reddit": "রেডিট",
|
||||||
|
"invidious": "ইনভিডিয়াস",
|
||||||
|
"generic_subscriptions_count": "{{count}}টি অনুসরণ",
|
||||||
|
"generic_subscriptions_count_plural": "{{count}}টি অনুসরণ",
|
||||||
|
"preferences_quality_option_medium": "মধ্যম",
|
||||||
|
"preferences_quality_option_small": "ছোট",
|
||||||
|
"preferences_quality_dash_option_1080p": "১০৮০পি",
|
||||||
|
"preferences_quality_dash_option_720p": "৭২০পি"
|
||||||
|
}
|
|
@ -136,6 +136,7 @@
|
||||||
"preferences_default_home_label": "Default homepage: ",
|
"preferences_default_home_label": "Default homepage: ",
|
||||||
"preferences_feed_menu_label": "Feed menu: ",
|
"preferences_feed_menu_label": "Feed menu: ",
|
||||||
"preferences_show_nick_label": "Show nickname on top: ",
|
"preferences_show_nick_label": "Show nickname on top: ",
|
||||||
|
"Popular enabled: ": "Popular enabled: ",
|
||||||
"Top enabled: ": "Top enabled: ",
|
"Top enabled: ": "Top enabled: ",
|
||||||
"CAPTCHA enabled: ": "CAPTCHA enabled: ",
|
"CAPTCHA enabled: ": "CAPTCHA enabled: ",
|
||||||
"Login enabled: ": "Login enabled: ",
|
"Login enabled: ": "Login enabled: ",
|
||||||
|
|
|
@ -116,6 +116,7 @@
|
||||||
"preferences_default_home_label": "Page d'accueil par défaut : ",
|
"preferences_default_home_label": "Page d'accueil par défaut : ",
|
||||||
"preferences_feed_menu_label": "Préferences des abonnements : ",
|
"preferences_feed_menu_label": "Préferences des abonnements : ",
|
||||||
"preferences_show_nick_label": "Afficher le nom d'utilisateur en haut à droite : ",
|
"preferences_show_nick_label": "Afficher le nom d'utilisateur en haut à droite : ",
|
||||||
|
"Popular enabled: ": "Page \"populaire\" activée: ",
|
||||||
"Top enabled: ": "Top activé : ",
|
"Top enabled: ": "Top activé : ",
|
||||||
"CAPTCHA enabled: ": "CAPTCHA activé : ",
|
"CAPTCHA enabled: ": "CAPTCHA activé : ",
|
||||||
"Login enabled: ": "Autoriser l'ouverture de sessions utilisateur : ",
|
"Login enabled: ": "Autoriser l'ouverture de sessions utilisateur : ",
|
||||||
|
|
|
@ -137,8 +137,8 @@
|
||||||
"Title": "Naslov",
|
"Title": "Naslov",
|
||||||
"Playlist privacy": "Privatnost zbirke",
|
"Playlist privacy": "Privatnost zbirke",
|
||||||
"Editing playlist `x`": "Uređivanje zbirke `x`",
|
"Editing playlist `x`": "Uređivanje zbirke `x`",
|
||||||
"Show more": "Pokaži više",
|
"Show more": "Prikaži više",
|
||||||
"Show less": "Pokaži manje",
|
"Show less": "Prikaži manje",
|
||||||
"Watch on YouTube": "Gledaj na YouTubeu",
|
"Watch on YouTube": "Gledaj na YouTubeu",
|
||||||
"Switch Invidious Instance": "Promijeni Invidious instancu",
|
"Switch Invidious Instance": "Promijeni Invidious instancu",
|
||||||
"Hide annotations": "Sakrij napomene",
|
"Hide annotations": "Sakrij napomene",
|
||||||
|
@ -318,7 +318,7 @@
|
||||||
"Movies": "Filmovi",
|
"Movies": "Filmovi",
|
||||||
"Download": "Preuzmi",
|
"Download": "Preuzmi",
|
||||||
"Download as: ": "Preuzmi kao: ",
|
"Download as: ": "Preuzmi kao: ",
|
||||||
"%A %B %-d, %Y": "%A, %-d. %B %Y",
|
"%A %B %-d, %Y": "%A, %-d. %B %Y.",
|
||||||
"(edited)": "(uređeno)",
|
"(edited)": "(uređeno)",
|
||||||
"YouTube comment permalink": "Stalna poveznica YouTube komentara",
|
"YouTube comment permalink": "Stalna poveznica YouTube komentara",
|
||||||
"permalink": "stalna poveznica",
|
"permalink": "stalna poveznica",
|
||||||
|
@ -328,40 +328,40 @@
|
||||||
"Videos": "Videa",
|
"Videos": "Videa",
|
||||||
"Playlists": "Zbirke",
|
"Playlists": "Zbirke",
|
||||||
"Community": "Zajednica",
|
"Community": "Zajednica",
|
||||||
"search_filters_sort_option_relevance": "značaj",
|
"search_filters_sort_option_relevance": "Značaj",
|
||||||
"search_filters_sort_option_rating": "ocjena",
|
"search_filters_sort_option_rating": "Ocjena",
|
||||||
"search_filters_sort_option_date": "datum",
|
"search_filters_sort_option_date": "Datum prijenosa",
|
||||||
"search_filters_sort_option_views": "prikazi",
|
"search_filters_sort_option_views": "Broj gledanja",
|
||||||
"search_filters_type_label": "vrsta_sadržaja",
|
"search_filters_type_label": "Vrsta",
|
||||||
"search_filters_duration_label": "trajanje",
|
"search_filters_duration_label": "Trajanje",
|
||||||
"search_filters_features_label": "funkcije",
|
"search_filters_features_label": "Funkcije",
|
||||||
"search_filters_sort_label": "redoslijed",
|
"search_filters_sort_label": "Redoslijed",
|
||||||
"search_filters_date_option_hour": "sat",
|
"search_filters_date_option_hour": "Zadnjih sat vremena",
|
||||||
"search_filters_date_option_today": "danas",
|
"search_filters_date_option_today": "Danas",
|
||||||
"search_filters_date_option_week": "tjedan",
|
"search_filters_date_option_week": "Ovaj tjedan",
|
||||||
"search_filters_date_option_month": "mjesec",
|
"search_filters_date_option_month": "Ovaj mjesec",
|
||||||
"search_filters_date_option_year": "godina",
|
"search_filters_date_option_year": "Ova godina",
|
||||||
"search_filters_type_option_video": "video",
|
"search_filters_type_option_video": "Video",
|
||||||
"search_filters_type_option_channel": "kanal",
|
"search_filters_type_option_channel": "Kanal",
|
||||||
"search_filters_type_option_playlist": "Zbirka",
|
"search_filters_type_option_playlist": "Zbirka",
|
||||||
"search_filters_type_option_movie": "film",
|
"search_filters_type_option_movie": "Film",
|
||||||
"search_filters_type_option_show": "emisija",
|
"search_filters_type_option_show": "Emisija",
|
||||||
"search_filters_features_option_hd": "hd",
|
"search_filters_features_option_hd": "HD",
|
||||||
"search_filters_features_option_subtitles": "titlovi",
|
"search_filters_features_option_subtitles": "Titlovi/CC",
|
||||||
"search_filters_features_option_c_commons": "creative_commons",
|
"search_filters_features_option_c_commons": "Creative Commons",
|
||||||
"search_filters_features_option_three_d": "3d",
|
"search_filters_features_option_three_d": "3D",
|
||||||
"search_filters_features_option_live": "uživo",
|
"search_filters_features_option_live": "Uživo",
|
||||||
"search_filters_features_option_four_k": "4k",
|
"search_filters_features_option_four_k": "4k",
|
||||||
"search_filters_features_option_location": "lokacija",
|
"search_filters_features_option_location": "Lokacija",
|
||||||
"search_filters_features_option_hdr": "hdr",
|
"search_filters_features_option_hdr": "HDR",
|
||||||
"Current version: ": "Trenutačna verzija: ",
|
"Current version: ": "Trenutačna verzija: ",
|
||||||
"next_steps_error_message": "Nakon toga bi trebali pokušati sljedeće: ",
|
"next_steps_error_message": "Nakon toga bi trebali pokušati sljedeće: ",
|
||||||
"next_steps_error_message_refresh": "Aktualiziraj stranicu",
|
"next_steps_error_message_refresh": "Aktualiziraj stranicu",
|
||||||
"next_steps_error_message_go_to_youtube": "Idi na YouTube",
|
"next_steps_error_message_go_to_youtube": "Idi na YouTube",
|
||||||
"footer_donate_page": "Doniraj",
|
"footer_donate_page": "Doniraj",
|
||||||
"adminprefs_modified_source_code_url_label": "URL do repozitorija izmijenjenog izvornog koda",
|
"adminprefs_modified_source_code_url_label": "URL do repozitorija izmijenjenog izvornog koda",
|
||||||
"search_filters_duration_option_short": "Kratki (< 4 minute)",
|
"search_filters_duration_option_short": "Kratko (< 4 minute)",
|
||||||
"search_filters_duration_option_long": "Dugi (> 20 minute)",
|
"search_filters_duration_option_long": "Dugo (> 20 minute)",
|
||||||
"footer_source_code": "Izvorni kod",
|
"footer_source_code": "Izvorni kod",
|
||||||
"footer_modfied_source_code": "Izmijenjeni izvorni kod",
|
"footer_modfied_source_code": "Izmijenjeni izvorni kod",
|
||||||
"footer_documentation": "Dokumentacija",
|
"footer_documentation": "Dokumentacija",
|
||||||
|
@ -384,8 +384,8 @@
|
||||||
"search_filters_features_option_three_sixty": "360 °",
|
"search_filters_features_option_three_sixty": "360 °",
|
||||||
"none": "bez",
|
"none": "bez",
|
||||||
"videoinfo_youTube_embed_link": "Ugradi",
|
"videoinfo_youTube_embed_link": "Ugradi",
|
||||||
"user_created_playlists": "`x` stvorene zbirke",
|
"user_created_playlists": "`x` je stvorio/la zbirke",
|
||||||
"user_saved_playlists": "`x` spremljene zbirke",
|
"user_saved_playlists": "`x` je spremio/la zbirke",
|
||||||
"Video unavailable": "Video nedostupan",
|
"Video unavailable": "Video nedostupan",
|
||||||
"preferences_save_player_pos_label": "Spremi mjesto reprodukcije: ",
|
"preferences_save_player_pos_label": "Spremi mjesto reprodukcije: ",
|
||||||
"videoinfo_watch_on_youTube": "Gledaj na YouTubeu",
|
"videoinfo_watch_on_youTube": "Gledaj na YouTubeu",
|
||||||
|
@ -432,7 +432,7 @@
|
||||||
"generic_subscriptions_count_2": "{{count}} pretplata",
|
"generic_subscriptions_count_2": "{{count}} pretplata",
|
||||||
"generic_playlists_count_0": "{{count}} zbirka",
|
"generic_playlists_count_0": "{{count}} zbirka",
|
||||||
"generic_playlists_count_1": "{{count}} zbirke",
|
"generic_playlists_count_1": "{{count}} zbirke",
|
||||||
"generic_playlists_count_2": "{{count}} zbirka",
|
"generic_playlists_count_2": "{{count}} zbiraka",
|
||||||
"generic_videos_count_0": "{{count}} video",
|
"generic_videos_count_0": "{{count}} video",
|
||||||
"generic_videos_count_1": "{{count}} videa",
|
"generic_videos_count_1": "{{count}} videa",
|
||||||
"generic_videos_count_2": "{{count}} videa",
|
"generic_videos_count_2": "{{count}} videa",
|
||||||
|
@ -476,5 +476,15 @@
|
||||||
"Portuguese (auto-generated)": "Portugalski (automatski generiran)",
|
"Portuguese (auto-generated)": "Portugalski (automatski generiran)",
|
||||||
"Spanish (auto-generated)": "Španjolski (automatski generiran)",
|
"Spanish (auto-generated)": "Španjolski (automatski generiran)",
|
||||||
"preferences_watch_history_label": "Aktiviraj povijest gledanja: ",
|
"preferences_watch_history_label": "Aktiviraj povijest gledanja: ",
|
||||||
"search_filters_title": "Filtar"
|
"search_filters_title": "Filtri",
|
||||||
|
"search_filters_date_option_none": "Bilo koji datum",
|
||||||
|
"search_filters_date_label": "Datum prijenosa",
|
||||||
|
"search_message_no_results": "Nema rezultata.",
|
||||||
|
"search_message_use_another_instance": " Također možeš <a href=\"`x`\">tražiti na jednoj drugoj instanci</a>.",
|
||||||
|
"search_message_change_filters_or_query": "Pokušaj proširiti upit za pretragu i/ili promijeni filtre.",
|
||||||
|
"search_filters_features_option_vr180": "VR180",
|
||||||
|
"search_filters_duration_option_none": "Bilo koje duljine",
|
||||||
|
"search_filters_duration_option_medium": "Srednje (4 – 20 minuta)",
|
||||||
|
"search_filters_apply_button": "Primijeni odabrane filtre",
|
||||||
|
"search_filters_type_option_all": "Bilo koja vrsta"
|
||||||
}
|
}
|
||||||
|
|
|
@ -418,5 +418,8 @@
|
||||||
"English (United States)": "Inggris (US)",
|
"English (United States)": "Inggris (US)",
|
||||||
"preferences_watch_history_label": "Aktifkan riwayat tontonan: ",
|
"preferences_watch_history_label": "Aktifkan riwayat tontonan: ",
|
||||||
"English (United Kingdom)": "Inggris (UK)",
|
"English (United Kingdom)": "Inggris (UK)",
|
||||||
"search_filters_title": "Saring"
|
"search_filters_title": "Saring",
|
||||||
|
"search_message_no_results": "Tidak ada hasil yang ditemukan.",
|
||||||
|
"search_message_change_filters_or_query": "Coba perbanyak kueri pencarian dan/atau ubah filter Anda.",
|
||||||
|
"search_message_use_another_instance": " Anda juga bisa <a href=\"`x`\">mencari di peladen lain</a>."
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
"preferences_show_nick_label": "Mostrar nome de utilizador em cima: ",
|
"preferences_show_nick_label": "Mostrar nome de utilizador em cima: ",
|
||||||
"preferences_automatic_instance_redirect_label": "Redirecionamento de instância automática (solução de último recurso para redirect.invidious.io): ",
|
"preferences_automatic_instance_redirect_label": "Redirecionamento de instância automática (solução de último recurso para redirect.invidious.io): ",
|
||||||
"preferences_category_misc": "Preferências diversas",
|
"preferences_category_misc": "Preferências diversas",
|
||||||
"preferences_vr_mode_label": "Vídeos interativos de 360 graus: ",
|
"preferences_vr_mode_label": "Vídeos interativos de 360 graus (necessita de WebGL): ",
|
||||||
"preferences_extend_desc_label": "Estender automaticamente a descrição do vídeo: ",
|
"preferences_extend_desc_label": "Estender automaticamente a descrição do vídeo: ",
|
||||||
"next_steps_error_message_go_to_youtube": "Ir ao YouTube",
|
"next_steps_error_message_go_to_youtube": "Ir ao YouTube",
|
||||||
"next_steps_error_message": "Pode tentar as seguintes opções: ",
|
"next_steps_error_message": "Pode tentar as seguintes opções: ",
|
||||||
|
@ -246,15 +246,15 @@
|
||||||
"JavaScript license information": "Informação de licença do JavaScript",
|
"JavaScript license information": "Informação de licença do JavaScript",
|
||||||
"An alternative front-end to YouTube": "Uma interface alternativa ao YouTube",
|
"An alternative front-end to YouTube": "Uma interface alternativa ao YouTube",
|
||||||
"History": "Histórico",
|
"History": "Histórico",
|
||||||
"Export data as JSON": "Exportar dados como JSON",
|
"Export data as JSON": "Exportar dados Invidious como JSON",
|
||||||
"Export subscriptions as OPML (for NewPipe & FreeTube)": "Exportar subscrições como OPML (para NewPipe e FreeTube)",
|
"Export subscriptions as OPML (for NewPipe & FreeTube)": "Exportar subscrições como OPML (para NewPipe e FreeTube)",
|
||||||
"Export subscriptions as OPML": "Exportar subscrições como OPML",
|
"Export subscriptions as OPML": "Exportar subscrições como OPML",
|
||||||
"Export": "Exportar",
|
"Export": "Exportar",
|
||||||
"Import NewPipe data (.zip)": "Importar dados do NewPipe (.zip)",
|
"Import NewPipe data (.zip)": "Importar dados do NewPipe (.zip)",
|
||||||
"Import NewPipe subscriptions (.json)": "Importar subscrições do NewPipe (.json)",
|
"Import NewPipe subscriptions (.json)": "Importar subscrições do NewPipe (.json)",
|
||||||
"Import FreeTube subscriptions (.db)": "Importar subscrições do FreeTube (.db)",
|
"Import FreeTube subscriptions (.db)": "Importar subscrições do FreeTube (.db)",
|
||||||
"Import YouTube subscriptions": "Importar subscrições do YouTube",
|
"Import YouTube subscriptions": "Importar subscrições do YouTube/OPML",
|
||||||
"Import Invidious data": "Importar dados do Invidious",
|
"Import Invidious data": "Importar dados JSON do Invidious",
|
||||||
"Import": "Importar",
|
"Import": "Importar",
|
||||||
"No": "Não",
|
"No": "Não",
|
||||||
"Yes": "Sim",
|
"Yes": "Sim",
|
||||||
|
@ -432,9 +432,43 @@
|
||||||
"crash_page_before_reporting": "Antes de reportar um erro, verifique se:",
|
"crash_page_before_reporting": "Antes de reportar um erro, verifique se:",
|
||||||
"crash_page_refresh": "tentou <a href=\"`x`\">recarregar a página</a>",
|
"crash_page_refresh": "tentou <a href=\"`x`\">recarregar a página</a>",
|
||||||
"crash_page_switch_instance": "tentou <a href=\"`x`\">usar outra instância</a>",
|
"crash_page_switch_instance": "tentou <a href=\"`x`\">usar outra instância</a>",
|
||||||
"crash_page_read_the_faq": "leu as <a href=\"`x`\">Perguntas frequentes (FAQ)</a>",
|
"crash_page_read_the_faq": "leia as <a href=\"`x`\">Perguntas frequentes (FAQ)</a>",
|
||||||
"crash_page_search_issue": "procurou se <a href=\"`x`\">o erro já foi reportado no GitHub</a>",
|
"crash_page_search_issue": "procurou se <a href=\"`x`\">o erro já foi reportado no GitHub</a>",
|
||||||
"crash_page_report_issue": "Se nenhuma opção acima ajudou, por favor <a href=\"`x`\">abra um novo problema no Github</a> (preferencialmente em inglês) e inclua o seguinte texto tal qual (NÃO o traduza):",
|
"crash_page_report_issue": "Se nenhuma opção acima ajudou, por favor <a href=\"`x`\">abra um novo problema no Github</a> (preferencialmente em inglês) e inclua o seguinte texto tal qual (NÃO o traduza):",
|
||||||
"user_created_playlists": "`x` listas de reprodução criadas",
|
"user_created_playlists": "`x` listas de reprodução criadas",
|
||||||
"search_filters_title": "Filtro"
|
"search_filters_title": "Filtro",
|
||||||
|
"Chinese (Taiwan)": "Chinês (Taiwan)",
|
||||||
|
"search_message_no_results": "Nenhum resultado encontrado.",
|
||||||
|
"search_message_change_filters_or_query": "Tente alargar os termos genéricos da pesquisa e/ou alterar os filtros.",
|
||||||
|
"search_message_use_another_instance": " Também pode <a href=\"`x`\">pesquisar noutra instância</a>.",
|
||||||
|
"English (United Kingdom)": "Inglês (Reino Unido)",
|
||||||
|
"English (United States)": "Inglês (Estados Unidos)",
|
||||||
|
"Cantonese (Hong Kong)": "Cantonês (Hong Kong)",
|
||||||
|
"Chinese": "Chinês",
|
||||||
|
"Chinese (Hong Kong)": "Chinês (Hong Kong)",
|
||||||
|
"Dutch (auto-generated)": "Holandês (gerado automaticamente)",
|
||||||
|
"French (auto-generated)": "Francês (gerado automaticamente)",
|
||||||
|
"German (auto-generated)": "Alemão (gerado automaticamente)",
|
||||||
|
"Indonesian (auto-generated)": "Indonésio (gerado automaticamente)",
|
||||||
|
"Interlingue": "Interlíngua",
|
||||||
|
"Italian (auto-generated)": "Italiano (gerado automaticamente)",
|
||||||
|
"Japanese (auto-generated)": "Japonês (gerado automaticamente)",
|
||||||
|
"Korean (auto-generated)": "Coreano (gerado automaticamente)",
|
||||||
|
"Portuguese (auto-generated)": "Português (gerado automaticamente)",
|
||||||
|
"Portuguese (Brazil)": "Português (Brasil)",
|
||||||
|
"Turkish (auto-generated)": "Turco (gerado automaticamente)",
|
||||||
|
"Vietnamese (auto-generated)": "Vietnamita (gerado automaticamente)",
|
||||||
|
"search_filters_duration_option_medium": "Médio (4 - 20 minutos)",
|
||||||
|
"search_filters_features_option_vr180": "VR180",
|
||||||
|
"search_filters_apply_button": "Aplicar filtros selecionados",
|
||||||
|
"Spanish (auto-generated)": "Espanhol (gerado automaticamente)",
|
||||||
|
"Spanish (Mexico)": "Espanhol (México)",
|
||||||
|
"preferences_watch_history_label": "Ativar histórico de reprodução: ",
|
||||||
|
"Chinese (China)": "Chinês (China)",
|
||||||
|
"Russian (auto-generated)": "Russo (gerado automaticamente)",
|
||||||
|
"Spanish (Spain)": "Espanhol (Espanha)",
|
||||||
|
"search_filters_date_label": "Data de publicação",
|
||||||
|
"search_filters_date_option_none": "Qualquer data",
|
||||||
|
"search_filters_type_option_all": "Qualquer tipo",
|
||||||
|
"search_filters_duration_option_none": "Qualquer duração"
|
||||||
}
|
}
|
||||||
|
|
126
locales/ru.json
126
locales/ru.json
|
@ -41,8 +41,8 @@
|
||||||
"User ID": "ID пользователя",
|
"User ID": "ID пользователя",
|
||||||
"Password": "Пароль",
|
"Password": "Пароль",
|
||||||
"Time (h:mm:ss):": "Время (ч:мм:сс):",
|
"Time (h:mm:ss):": "Время (ч:мм:сс):",
|
||||||
"Text CAPTCHA": "Текст капчи",
|
"Text CAPTCHA": "Текстовая капча (англ.)",
|
||||||
"Image CAPTCHA": "Изображение капчи",
|
"Image CAPTCHA": "Капча-картинка",
|
||||||
"Sign In": "Войти",
|
"Sign In": "Войти",
|
||||||
"Register": "Зарегистрироваться",
|
"Register": "Зарегистрироваться",
|
||||||
"E-mail": "Электронная почта",
|
"E-mail": "Электронная почта",
|
||||||
|
@ -51,7 +51,7 @@
|
||||||
"preferences_category_player": "Настройки проигрывателя",
|
"preferences_category_player": "Настройки проигрывателя",
|
||||||
"preferences_video_loop_label": "Всегда повторять: ",
|
"preferences_video_loop_label": "Всегда повторять: ",
|
||||||
"preferences_autoplay_label": "Автовоспроизведение: ",
|
"preferences_autoplay_label": "Автовоспроизведение: ",
|
||||||
"preferences_continue_label": "Всегда включать следующее видео? ",
|
"preferences_continue_label": "Переходить к следующему видео? ",
|
||||||
"preferences_continue_autoplay_label": "Автопроигрывание следующего видео: ",
|
"preferences_continue_autoplay_label": "Автопроигрывание следующего видео: ",
|
||||||
"preferences_listen_label": "Режим «только аудио» по умолчанию: ",
|
"preferences_listen_label": "Режим «только аудио» по умолчанию: ",
|
||||||
"preferences_local_label": "Проигрывать видео через прокси? ",
|
"preferences_local_label": "Проигрывать видео через прокси? ",
|
||||||
|
@ -71,13 +71,13 @@
|
||||||
"preferences_player_style_label": "Стиль проигрывателя: ",
|
"preferences_player_style_label": "Стиль проигрывателя: ",
|
||||||
"Dark mode: ": "Тёмное оформление: ",
|
"Dark mode: ": "Тёмное оформление: ",
|
||||||
"preferences_dark_mode_label": "Тема: ",
|
"preferences_dark_mode_label": "Тема: ",
|
||||||
"dark": "темная",
|
"dark": "тёмная",
|
||||||
"light": "светлая",
|
"light": "светлая",
|
||||||
"preferences_thin_mode_label": "Облегчённое оформление: ",
|
"preferences_thin_mode_label": "Облегчённое оформление: ",
|
||||||
"preferences_category_misc": "Прочие настройки",
|
"preferences_category_misc": "Прочие настройки",
|
||||||
"preferences_automatic_instance_redirect_label": "Автоматическое перенаправление на зеркало сайта (переход на redirect.invidious.io): ",
|
"preferences_automatic_instance_redirect_label": "Автоматическое перенаправление на зеркало сайта (переход на redirect.invidious.io): ",
|
||||||
"preferences_category_subscription": "Настройки подписок",
|
"preferences_category_subscription": "Настройки подписок",
|
||||||
"preferences_annotations_subscribed_label": "Всегда показывать аннотации в видео каналов, на которые вы подписаны? ",
|
"preferences_annotations_subscribed_label": "Всегда показывать аннотации на каналах из ваших подписок? ",
|
||||||
"Redirect homepage to feed: ": "Отображать видео с каналов, на которые вы подписаны, как главную страницу: ",
|
"Redirect homepage to feed: ": "Отображать видео с каналов, на которые вы подписаны, как главную страницу: ",
|
||||||
"preferences_max_results_label": "Число видео, на которые вы подписаны, в ленте: ",
|
"preferences_max_results_label": "Число видео, на которые вы подписаны, в ленте: ",
|
||||||
"preferences_sort_label": "Сортировать видео: ",
|
"preferences_sort_label": "Сортировать видео: ",
|
||||||
|
@ -96,10 +96,10 @@
|
||||||
"`x` is live": "`x` в прямом эфире",
|
"`x` is live": "`x` в прямом эфире",
|
||||||
"preferences_category_data": "Настройки данных",
|
"preferences_category_data": "Настройки данных",
|
||||||
"Clear watch history": "Очистить историю просмотров",
|
"Clear watch history": "Очистить историю просмотров",
|
||||||
"Import/export data": "Импорт/Экспорт данных",
|
"Import/export data": "Импорт и экспорт данных",
|
||||||
"Change password": "Изменить пароль",
|
"Change password": "Изменить пароль",
|
||||||
"Manage subscriptions": "Управлять подписками",
|
"Manage subscriptions": "Управление подписками",
|
||||||
"Manage tokens": "Управлять токенами",
|
"Manage tokens": "Управление токенами",
|
||||||
"Watch history": "История просмотров",
|
"Watch history": "История просмотров",
|
||||||
"Delete account": "Удалить аккаунт",
|
"Delete account": "Удалить аккаунт",
|
||||||
"preferences_category_admin": "Администраторские настройки",
|
"preferences_category_admin": "Администраторские настройки",
|
||||||
|
@ -112,8 +112,8 @@
|
||||||
"Registration enabled: ": "Включить регистрацию? ",
|
"Registration enabled: ": "Включить регистрацию? ",
|
||||||
"Report statistics: ": "Сообщать статистику? ",
|
"Report statistics: ": "Сообщать статистику? ",
|
||||||
"Save preferences": "Сохранить настройки",
|
"Save preferences": "Сохранить настройки",
|
||||||
"Subscription manager": "Менеджер подписок",
|
"Subscription manager": "Управление подписками",
|
||||||
"Token manager": "Менеджер токенов",
|
"Token manager": "Управление токенами",
|
||||||
"Token": "Токен",
|
"Token": "Токен",
|
||||||
"Import/export": "Импорт и экспорт",
|
"Import/export": "Импорт и экспорт",
|
||||||
"unsubscribe": "отписаться",
|
"unsubscribe": "отписаться",
|
||||||
|
@ -122,9 +122,9 @@
|
||||||
"search": "поиск",
|
"search": "поиск",
|
||||||
"Log out": "Выйти",
|
"Log out": "Выйти",
|
||||||
"Released under the AGPLv3 on Github.": "Выпущено под лицензией AGPLv3 на GitHub.",
|
"Released under the AGPLv3 on Github.": "Выпущено под лицензией AGPLv3 на GitHub.",
|
||||||
"Source available here.": "Исходный код доступен здесь.",
|
"Source available here.": "Исходный код.",
|
||||||
"View JavaScript license information.": "Посмотреть информацию по лицензии JavaScript.",
|
"View JavaScript license information.": "Информация о лицензиях JavaScript.",
|
||||||
"View privacy policy.": "Посмотреть политику конфиденциальности.",
|
"View privacy policy.": "Политика конфиденциальности.",
|
||||||
"Trending": "В тренде",
|
"Trending": "В тренде",
|
||||||
"Public": "Публичный",
|
"Public": "Публичный",
|
||||||
"Unlisted": "Нет в списке",
|
"Unlisted": "Нет в списке",
|
||||||
|
@ -135,42 +135,42 @@
|
||||||
"Delete playlist": "Удалить плейлист",
|
"Delete playlist": "Удалить плейлист",
|
||||||
"Create playlist": "Создать плейлист",
|
"Create playlist": "Создать плейлист",
|
||||||
"Title": "Заголовок",
|
"Title": "Заголовок",
|
||||||
"Playlist privacy": "Конфиденциальность плейлиста",
|
"Playlist privacy": "Видимость плейлиста",
|
||||||
"Editing playlist `x`": "Редактирование плейлиста `x`",
|
"Editing playlist `x`": "Редактирование плейлиста `x`",
|
||||||
"Show more": "Показать больше",
|
"Show more": "Развернуть",
|
||||||
"Show less": "Показать меньше",
|
"Show less": "Свернуть",
|
||||||
"Watch on YouTube": "Смотреть на YouTube",
|
"Watch on YouTube": "Смотреть на YouTube",
|
||||||
"Switch Invidious Instance": "Сменить экземпляр Invidious",
|
"Switch Invidious Instance": "Сменить зеркало Invidious",
|
||||||
"Hide annotations": "Скрыть аннотации",
|
"Hide annotations": "Скрыть аннотации",
|
||||||
"Show annotations": "Показать аннотации",
|
"Show annotations": "Показать аннотации",
|
||||||
"Genre: ": "Жанр: ",
|
"Genre: ": "Жанр: ",
|
||||||
"License: ": "Лицензия: ",
|
"License: ": "Лицензия: ",
|
||||||
"Family friendly? ": "Семейный просмотр: ",
|
"Family friendly? ": "Семейный просмотр: ",
|
||||||
"Wilson score: ": "Рейтинг Уилсона: ",
|
"Wilson score: ": "Оценка Уилсона: ",
|
||||||
"Engagement: ": "Вовлечённость: ",
|
"Engagement: ": "Вовлечённость: ",
|
||||||
"Whitelisted regions: ": "Доступно в регионах: ",
|
"Whitelisted regions: ": "Доступно в регионах: ",
|
||||||
"Blacklisted regions: ": "Недоступно в регионах: ",
|
"Blacklisted regions: ": "Недоступно в регионах: ",
|
||||||
"Shared `x`": "Опубликовано `x`",
|
"Shared `x`": "Опубликовано `x`",
|
||||||
"Premieres in `x`": "Премьера через `x`",
|
"Premieres in `x`": "Премьера через `x`",
|
||||||
"Premieres `x`": "Премьера `x`",
|
"Premieres `x`": "Премьера `x`",
|
||||||
"Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Похоже, у вас отключён JavaScript. Чтобы увидить комментарии, нажмите сюда, но учтите: они могут загружаться немного медленнее.",
|
"Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Похоже, у вас отключён JavaScript. Нажмите сюда, чтобы увидеть комментарии. Но учтите: они могут загружаться немного медленнее.",
|
||||||
"View YouTube comments": "Смотреть комментарии с YouTube",
|
"View YouTube comments": "Показать комментарии с YouTube",
|
||||||
"View more comments on Reddit": "Посмотреть больше комментариев на Reddit",
|
"View more comments on Reddit": "Посмотреть больше комментариев на Reddit",
|
||||||
"View `x` comments": {
|
"View `x` comments": {
|
||||||
"([^.,0-9]|^)1([^.,0-9]|$)": "Показать `x` комментариев",
|
"([^.,0-9]|^)1([^.,0-9]|$)": "Показано `x` комментариев",
|
||||||
"": "Показать `x` комментариев"
|
"": "Показано`x` комментариев"
|
||||||
},
|
},
|
||||||
"View Reddit comments": "Смотреть комментарии с Reddit",
|
"View Reddit comments": "Смотреть комментарии с Reddit",
|
||||||
"Hide replies": "Скрыть ответы",
|
"Hide replies": "Скрыть ответы",
|
||||||
"Show replies": "Показать ответы",
|
"Show replies": "Показать ответы",
|
||||||
"Incorrect password": "Неправильный пароль",
|
"Incorrect password": "Неправильный пароль",
|
||||||
"Quota exceeded, try again in a few hours": "Лимит превышен, попробуйте снова через несколько часов",
|
"Quota exceeded, try again in a few hours": "Лимит превышен, попробуйте снова через несколько часов",
|
||||||
"Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Войти не удаётся. Проверьте, не включена ли двухфакторная аутентификация (по коду или смс).",
|
"Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Не удалось войти. Проверьте, не включена ли двухфакторная аутентификация (по коду или смс).",
|
||||||
"Invalid TFA code": "Неправильный код двухфакторной аутентификации",
|
"Invalid TFA code": "Неправильный код двухфакторной аутентификации",
|
||||||
"Login failed. This may be because two-factor authentication is not turned on for your account.": "Не удаётся войти. Это может быть из-за того, что в вашем аккаунте не включена двухфакторная аутентификация.",
|
"Login failed. This may be because two-factor authentication is not turned on for your account.": "Не удалось войти. Это может быть из-за того, что в вашем аккаунте не включена двухфакторная аутентификация.",
|
||||||
"Wrong answer": "Неправильный ответ",
|
"Wrong answer": "Неправильный ответ",
|
||||||
"Erroneous CAPTCHA": "Неправильная капча",
|
"Erroneous CAPTCHA": "Неправильная капча",
|
||||||
"CAPTCHA is a required field": "Необходимо пройти капчу",
|
"CAPTCHA is a required field": "Необходимо решить капчу",
|
||||||
"User ID is a required field": "Необходимо ввести ID пользователя",
|
"User ID is a required field": "Необходимо ввести ID пользователя",
|
||||||
"Password is a required field": "Необходимо ввести пароль",
|
"Password is a required field": "Необходимо ввести пароль",
|
||||||
"Wrong username or password": "Неправильный логин или пароль",
|
"Wrong username or password": "Неправильный логин или пароль",
|
||||||
|
@ -185,7 +185,7 @@
|
||||||
"Could not get channel info.": "Не удаётся получить информацию об этом канале.",
|
"Could not get channel info.": "Не удаётся получить информацию об этом канале.",
|
||||||
"Could not fetch comments": "Не удаётся загрузить комментарии",
|
"Could not fetch comments": "Не удаётся загрузить комментарии",
|
||||||
"`x` ago": "`x` назад",
|
"`x` ago": "`x` назад",
|
||||||
"Load more": "Загрузить больше",
|
"Load more": "Загрузить ещё",
|
||||||
"Could not create mix.": "Не удаётся создать микс.",
|
"Could not create mix.": "Не удаётся создать микс.",
|
||||||
"Empty playlist": "Плейлист пуст",
|
"Empty playlist": "Плейлист пуст",
|
||||||
"Not a playlist.": "Некорректный плейлист.",
|
"Not a playlist.": "Некорректный плейлист.",
|
||||||
|
@ -219,7 +219,7 @@
|
||||||
"Croatian": "Хорватский",
|
"Croatian": "Хорватский",
|
||||||
"Czech": "Чешский",
|
"Czech": "Чешский",
|
||||||
"Danish": "Датский",
|
"Danish": "Датский",
|
||||||
"Dutch": "Нидерландский",
|
"Dutch": "Голландский",
|
||||||
"Esperanto": "Эсперанто",
|
"Esperanto": "Эсперанто",
|
||||||
"Estonian": "Эстонский",
|
"Estonian": "Эстонский",
|
||||||
"Filipino": "Филиппинский",
|
"Filipino": "Филиппинский",
|
||||||
|
@ -229,8 +229,8 @@
|
||||||
"Georgian": "Грузинский",
|
"Georgian": "Грузинский",
|
||||||
"German": "Немецкий",
|
"German": "Немецкий",
|
||||||
"Greek": "Греческий",
|
"Greek": "Греческий",
|
||||||
"Gujarati": "Гуджаратский",
|
"Gujarati": "Гуджарати",
|
||||||
"Haitian Creole": "Гаит. креольский",
|
"Haitian Creole": "Гаитянский креольский",
|
||||||
"Hausa": "Хауса",
|
"Hausa": "Хауса",
|
||||||
"Hawaiian": "Гавайский",
|
"Hawaiian": "Гавайский",
|
||||||
"Hebrew": "Иврит",
|
"Hebrew": "Иврит",
|
||||||
|
@ -251,7 +251,7 @@
|
||||||
"Kurdish": "Курдский",
|
"Kurdish": "Курдский",
|
||||||
"Kyrgyz": "Киргизский",
|
"Kyrgyz": "Киргизский",
|
||||||
"Lao": "Лаосский",
|
"Lao": "Лаосский",
|
||||||
"Latin": "Латинский",
|
"Latin": "Латынь",
|
||||||
"Latvian": "Латышский",
|
"Latvian": "Латышский",
|
||||||
"Lithuanian": "Литовский",
|
"Lithuanian": "Литовский",
|
||||||
"Luxembourgish": "Люксембургский",
|
"Luxembourgish": "Люксембургский",
|
||||||
|
@ -262,9 +262,9 @@
|
||||||
"Maltese": "Мальтийский",
|
"Maltese": "Мальтийский",
|
||||||
"Maori": "Маори",
|
"Maori": "Маори",
|
||||||
"Marathi": "Маратхи",
|
"Marathi": "Маратхи",
|
||||||
"Mongolian": "Монгольская",
|
"Mongolian": "Монгольский",
|
||||||
"Nepali": "Непальский",
|
"Nepali": "Непальский",
|
||||||
"Norwegian Bokmål": "Норвежский",
|
"Norwegian Bokmål": "Норвежский букмол",
|
||||||
"Nyanja": "Ньянджа",
|
"Nyanja": "Ньянджа",
|
||||||
"Pashto": "Пушту",
|
"Pashto": "Пушту",
|
||||||
"Persian": "Персидский",
|
"Persian": "Персидский",
|
||||||
|
@ -299,7 +299,7 @@
|
||||||
"Vietnamese": "Вьетнамский",
|
"Vietnamese": "Вьетнамский",
|
||||||
"Welsh": "Валлийский",
|
"Welsh": "Валлийский",
|
||||||
"Western Frisian": "Западнофризский",
|
"Western Frisian": "Западнофризский",
|
||||||
"Xhosa": "Коса",
|
"Xhosa": "Коса (кхоса)",
|
||||||
"Yiddish": "Идиш",
|
"Yiddish": "Идиш",
|
||||||
"Yoruba": "Йоруба",
|
"Yoruba": "Йоруба",
|
||||||
"Zulu": "Зулусский",
|
"Zulu": "Зулусский",
|
||||||
|
@ -311,7 +311,7 @@
|
||||||
"Rating: ": "Рейтинг: ",
|
"Rating: ": "Рейтинг: ",
|
||||||
"preferences_locale_label": "Язык: ",
|
"preferences_locale_label": "Язык: ",
|
||||||
"View as playlist": "Смотреть как плейлист",
|
"View as playlist": "Смотреть как плейлист",
|
||||||
"Default": "По-умолчанию",
|
"Default": "По умолчанию",
|
||||||
"Music": "Музыка",
|
"Music": "Музыка",
|
||||||
"Gaming": "Игры",
|
"Gaming": "Игры",
|
||||||
"News": "Новости",
|
"News": "Новости",
|
||||||
|
@ -328,14 +328,14 @@
|
||||||
"Videos": "Видео",
|
"Videos": "Видео",
|
||||||
"Playlists": "Плейлисты",
|
"Playlists": "Плейлисты",
|
||||||
"Community": "Сообщество",
|
"Community": "Сообщество",
|
||||||
"search_filters_sort_option_relevance": "Актуальность",
|
"search_filters_sort_option_relevance": "по актуальности",
|
||||||
"search_filters_sort_option_rating": "Рейтинг",
|
"search_filters_sort_option_rating": "по рейтингу",
|
||||||
"search_filters_sort_option_date": "Дата загрузки",
|
"search_filters_sort_option_date": "по дате загрузки",
|
||||||
"search_filters_sort_option_views": "Просмотры",
|
"search_filters_sort_option_views": "по просмотрам",
|
||||||
"search_filters_type_label": "Тип",
|
"search_filters_type_label": "Тип",
|
||||||
"search_filters_duration_label": "Длительность",
|
"search_filters_duration_label": "Длительность",
|
||||||
"search_filters_features_label": "Функции",
|
"search_filters_features_label": "Дополнительно",
|
||||||
"search_filters_sort_label": "Сортировать по",
|
"search_filters_sort_label": "Сортировать",
|
||||||
"search_filters_date_option_hour": "Последний час",
|
"search_filters_date_option_hour": "Последний час",
|
||||||
"search_filters_date_option_today": "Сегодня",
|
"search_filters_date_option_today": "Сегодня",
|
||||||
"search_filters_date_option_week": "Эта неделя",
|
"search_filters_date_option_week": "Эта неделя",
|
||||||
|
@ -345,7 +345,7 @@
|
||||||
"search_filters_type_option_channel": "Канал",
|
"search_filters_type_option_channel": "Канал",
|
||||||
"search_filters_type_option_playlist": "Плейлист",
|
"search_filters_type_option_playlist": "Плейлист",
|
||||||
"search_filters_type_option_movie": "Фильм",
|
"search_filters_type_option_movie": "Фильм",
|
||||||
"search_filters_type_option_show": "Показать",
|
"search_filters_type_option_show": "Сериал",
|
||||||
"search_filters_features_option_hd": "HD",
|
"search_filters_features_option_hd": "HD",
|
||||||
"search_filters_features_option_subtitles": "Субтитры",
|
"search_filters_features_option_subtitles": "Субтитры",
|
||||||
"search_filters_features_option_c_commons": "Creative Commons",
|
"search_filters_features_option_c_commons": "Creative Commons",
|
||||||
|
@ -368,28 +368,28 @@
|
||||||
"English (United States)": "Английский (США)",
|
"English (United States)": "Английский (США)",
|
||||||
"Cantonese (Hong Kong)": "Кантонский (Гонконг)",
|
"Cantonese (Hong Kong)": "Кантонский (Гонконг)",
|
||||||
"Chinese (Taiwan)": "Китайский (Тайвань)",
|
"Chinese (Taiwan)": "Китайский (Тайвань)",
|
||||||
"Dutch (auto-generated)": "Голландский (автоматический)",
|
"Dutch (auto-generated)": "Голландский (созданы автоматически)",
|
||||||
"German (auto-generated)": "Немецкий (автоматический)",
|
"German (auto-generated)": "Немецкий (созданы автоматически)",
|
||||||
"Indonesian (auto-generated)": "Индонезийский (автоматический)",
|
"Indonesian (auto-generated)": "Индонезийский (созданы автоматически)",
|
||||||
"Italian (auto-generated)": "Итальянский (автоматический)",
|
"Italian (auto-generated)": "Итальянский (созданы автоматически)",
|
||||||
"Interlingue": "Окциденталь",
|
"Interlingue": "Окциденталь",
|
||||||
"Russian (auto-generated)": "Русский (автоматический)",
|
"Russian (auto-generated)": "Русский (созданы автоматически)",
|
||||||
"Spanish (auto-generated)": "Испанский (автоматический)",
|
"Spanish (auto-generated)": "Испанский (созданы автоматически)",
|
||||||
"Spanish (Spain)": "Испанский (Испания)",
|
"Spanish (Spain)": "Испанский (Испания)",
|
||||||
"Turkish (auto-generated)": "Турецкий (автоматический)",
|
"Turkish (auto-generated)": "Турецкий (созданы автоматически)",
|
||||||
"Vietnamese (auto-generated)": "Вьетнамский (автоматический)",
|
"Vietnamese (auto-generated)": "Вьетнамский (созданы автоматически)",
|
||||||
"footer_documentation": "Документация",
|
"footer_documentation": "Документация",
|
||||||
"adminprefs_modified_source_code_url_label": "Ссылка на нашу ветку репозитория",
|
"adminprefs_modified_source_code_url_label": "Ссылка на нашу ветку репозитория",
|
||||||
"none": "ничего",
|
"none": "ничего",
|
||||||
"videoinfo_watch_on_youTube": "Смотреть на YouTube",
|
"videoinfo_watch_on_youTube": "Смотреть на YouTube",
|
||||||
"videoinfo_youTube_embed_link": "Встраиваемый элемент",
|
"videoinfo_youTube_embed_link": "Версия для встраивания",
|
||||||
"videoinfo_invidious_embed_link": "Встраиваемая ссылка",
|
"videoinfo_invidious_embed_link": "Ссылка для встраивания",
|
||||||
"download_subtitles": "Субтитры - `x` (.vtt)",
|
"download_subtitles": "Субтитры - `x` (.vtt)",
|
||||||
"user_created_playlists": "`x` созданных плейлистов",
|
"user_created_playlists": "`x` созданных плейлистов",
|
||||||
"crash_page_you_found_a_bug": "Похоже вы нашли баг в Invidious!",
|
"crash_page_you_found_a_bug": "Похоже, вы нашли ошибку в Invidious!",
|
||||||
"crash_page_before_reporting": "Прежде чем сообщать об ошибке, убедитесь, что вы:",
|
"crash_page_before_reporting": "Прежде чем сообщать об ошибке, убедитесь, что вы:",
|
||||||
"crash_page_refresh": "пробовали <a href=\"`x`\"> перезагрузить страницу</a>",
|
"crash_page_refresh": "пробовали <a href=\"`x`\"> перезагрузить страницу</a>",
|
||||||
"crash_page_report_issue": "Если ни один вариант не помог, пожалуйста <a href=\"`x`\">откройте новую проблему на GitHub</a> (желательно на английском) и приложите следующий текст к вашему сообщению (НЕ переводите его):",
|
"crash_page_report_issue": "Если ни один вариант не помог, пожалуйста <a href=\"`x`\">откройте новую проблему на GitHub</a> (на английском, пжлста) и приложите следующий текст к вашему сообщению (НЕ переводите его):",
|
||||||
"generic_videos_count_0": "{{count}} видео",
|
"generic_videos_count_0": "{{count}} видео",
|
||||||
"generic_videos_count_1": "{{count}} видео",
|
"generic_videos_count_1": "{{count}} видео",
|
||||||
"generic_videos_count_2": "{{count}} видео",
|
"generic_videos_count_2": "{{count}} видео",
|
||||||
|
@ -417,8 +417,8 @@
|
||||||
"generic_views_count_0": "{{count}} просмотр",
|
"generic_views_count_0": "{{count}} просмотр",
|
||||||
"generic_views_count_1": "{{count}} просмотра",
|
"generic_views_count_1": "{{count}} просмотра",
|
||||||
"generic_views_count_2": "{{count}} просмотров",
|
"generic_views_count_2": "{{count}} просмотров",
|
||||||
"French (auto-generated)": "Французский (автоматический)",
|
"French (auto-generated)": "Французский (созданы автоматически)",
|
||||||
"Portuguese (auto-generated)": "Португальский (автоматический)",
|
"Portuguese (auto-generated)": "Португальский (созданы автоматически)",
|
||||||
"generic_count_days_0": "{{count}} день",
|
"generic_count_days_0": "{{count}} день",
|
||||||
"generic_count_days_1": "{{count}} дня",
|
"generic_count_days_1": "{{count}} дня",
|
||||||
"generic_count_days_2": "{{count}} дней",
|
"generic_count_days_2": "{{count}} дней",
|
||||||
|
@ -438,12 +438,12 @@
|
||||||
"search_filters_features_option_purchased": "Приобретено",
|
"search_filters_features_option_purchased": "Приобретено",
|
||||||
"videoinfo_started_streaming_x_ago": "Трансляция началась `x` назад",
|
"videoinfo_started_streaming_x_ago": "Трансляция началась `x` назад",
|
||||||
"crash_page_switch_instance": "пробовали <a href=\"`x`\">использовать другое зеркало</a>",
|
"crash_page_switch_instance": "пробовали <a href=\"`x`\">использовать другое зеркало</a>",
|
||||||
"crash_page_read_the_faq": "прочли <a href=\"`x`\">Частые Вопросы (ЧаВо)</a>",
|
"crash_page_read_the_faq": "прочли ответы на <a href=\"`x`\">Частые Вопросы (ЧаВо)</a>",
|
||||||
"Chinese": "Китайский",
|
"Chinese": "Китайский",
|
||||||
"Chinese (Hong Kong)": "Китайский (Гонконг)",
|
"Chinese (Hong Kong)": "Китайский (Гонконг)",
|
||||||
"Japanese (auto-generated)": "Японский (автоматический)",
|
"Japanese (auto-generated)": "Японский (созданы автоматически)",
|
||||||
"Chinese (China)": "Китайский (Китай)",
|
"Chinese (China)": "Китайский (Китай)",
|
||||||
"Korean (auto-generated)": "Корейский (автоматический)",
|
"Korean (auto-generated)": "Корейский (созданы автоматически)",
|
||||||
"generic_count_months_0": "{{count}} месяц",
|
"generic_count_months_0": "{{count}} месяц",
|
||||||
"generic_count_months_1": "{{count}} месяца",
|
"generic_count_months_1": "{{count}} месяца",
|
||||||
"generic_count_months_2": "{{count}} месяцев",
|
"generic_count_months_2": "{{count}} месяцев",
|
||||||
|
@ -455,7 +455,7 @@
|
||||||
"footer_original_source_code": "Оригинальный исходный код",
|
"footer_original_source_code": "Оригинальный исходный код",
|
||||||
"footer_modfied_source_code": "Изменённый исходный код",
|
"footer_modfied_source_code": "Изменённый исходный код",
|
||||||
"user_saved_playlists": "`x` сохранённых плейлистов",
|
"user_saved_playlists": "`x` сохранённых плейлистов",
|
||||||
"crash_page_search_issue": "искали <a href=\"`x`\">похожую проблему на GitHub</a>",
|
"crash_page_search_issue": "поискали <a href=\"`x`\">похожую проблему на GitHub</a>",
|
||||||
"comments_points_count_0": "{{count}} плюс",
|
"comments_points_count_0": "{{count}} плюс",
|
||||||
"comments_points_count_1": "{{count}} плюса",
|
"comments_points_count_1": "{{count}} плюса",
|
||||||
"comments_points_count_2": "{{count}} плюсов",
|
"comments_points_count_2": "{{count}} плюсов",
|
||||||
|
@ -464,7 +464,7 @@
|
||||||
"preferences_quality_option_dash": "DASH (автоматическое качество)",
|
"preferences_quality_option_dash": "DASH (автоматическое качество)",
|
||||||
"preferences_quality_option_hd720": "HD720",
|
"preferences_quality_option_hd720": "HD720",
|
||||||
"preferences_quality_option_medium": "Среднее",
|
"preferences_quality_option_medium": "Среднее",
|
||||||
"preferences_quality_dash_label": "Предпочтительное автоматическое качество видео: ",
|
"preferences_quality_dash_label": "Предпочтительное качество для DASH: ",
|
||||||
"preferences_quality_dash_option_worst": "Очень низкое",
|
"preferences_quality_dash_option_worst": "Очень низкое",
|
||||||
"preferences_quality_dash_option_4320p": "4320p",
|
"preferences_quality_dash_option_4320p": "4320p",
|
||||||
"preferences_quality_dash_option_2160p": "2160p",
|
"preferences_quality_dash_option_2160p": "2160p",
|
||||||
|
@ -475,16 +475,16 @@
|
||||||
"Video unavailable": "Видео недоступно",
|
"Video unavailable": "Видео недоступно",
|
||||||
"preferences_save_player_pos_label": "Запоминать позицию: ",
|
"preferences_save_player_pos_label": "Запоминать позицию: ",
|
||||||
"preferences_region_label": "Страна: ",
|
"preferences_region_label": "Страна: ",
|
||||||
"preferences_watch_history_label": "Включить историю просмотров ",
|
"preferences_watch_history_label": "Включить историю просмотров: ",
|
||||||
"search_filters_title": "Фильтр",
|
"search_filters_title": "Фильтр",
|
||||||
"search_filters_duration_option_none": "Любой длины",
|
"search_filters_duration_option_none": "Любой длины",
|
||||||
"search_filters_type_option_all": "Любого типа",
|
"search_filters_type_option_all": "Любого типа",
|
||||||
"search_filters_date_option_none": "Любой даты",
|
"search_filters_date_option_none": "Любая дата",
|
||||||
"search_filters_date_label": "Дата загрузки",
|
"search_filters_date_label": "Дата загрузки",
|
||||||
"search_message_no_results": "Ничего не найдено.",
|
"search_message_no_results": "Ничего не найдено.",
|
||||||
"search_message_use_another_instance": " Дополнительно вы можете <a href=\"`x`\">поискать на других зеркалах</a>.",
|
"search_message_use_another_instance": " Дополнительно вы можете <a href=\"`x`\">поискать на других зеркалах</a>.",
|
||||||
"search_filters_features_option_vr180": "VR180",
|
"search_filters_features_option_vr180": "VR180",
|
||||||
"search_message_change_filters_or_query": "Попробуйте расширить поисковый запрос и изменить фильтры.",
|
"search_message_change_filters_or_query": "Попробуйте расширить поисковый запрос или изменить фильтры.",
|
||||||
"search_filters_duration_option_medium": "Средние (4 - 20 минут)",
|
"search_filters_duration_option_medium": "Средние (4 - 20 минут)",
|
||||||
"search_filters_apply_button": "Применить фильтры"
|
"search_filters_apply_button": "Применить фильтры"
|
||||||
}
|
}
|
||||||
|
|
|
@ -80,7 +80,7 @@
|
||||||
"preferences_category_admin": "Skrbniške nastavitve",
|
"preferences_category_admin": "Skrbniške nastavitve",
|
||||||
"preferences_default_home_label": "Privzeta domača stran: ",
|
"preferences_default_home_label": "Privzeta domača stran: ",
|
||||||
"preferences_feed_menu_label": "Meni vira: ",
|
"preferences_feed_menu_label": "Meni vira: ",
|
||||||
"Top enabled: ": "Vrh je omogočen: ",
|
"Top enabled: ": "Vrh omogočen: ",
|
||||||
"CAPTCHA enabled: ": "CAPTCHA omogočeni: ",
|
"CAPTCHA enabled: ": "CAPTCHA omogočeni: ",
|
||||||
"Login enabled: ": "Prijava je omogočena: ",
|
"Login enabled: ": "Prijava je omogočena: ",
|
||||||
"Registration enabled: ": "Registracija je omogočena: ",
|
"Registration enabled: ": "Registracija je omogočena: ",
|
||||||
|
@ -112,7 +112,7 @@
|
||||||
"Wilson score: ": "Wilsonov rezultat: ",
|
"Wilson score: ": "Wilsonov rezultat: ",
|
||||||
"Engagement: ": "Sodelovanje: ",
|
"Engagement: ": "Sodelovanje: ",
|
||||||
"Blacklisted regions: ": "Regije na seznamu nedovoljenih: ",
|
"Blacklisted regions: ": "Regije na seznamu nedovoljenih: ",
|
||||||
"Shared `x`": "V skupni rabi `x`",
|
"Shared `x`": "V skupni rabi od: `x`",
|
||||||
"Premieres `x`": "Premiere `x`",
|
"Premieres `x`": "Premiere `x`",
|
||||||
"View YouTube comments": "Oglej si YouTube komentarje",
|
"View YouTube comments": "Oglej si YouTube komentarje",
|
||||||
"View more comments on Reddit": "Prikaži več komentarjev na Reddit",
|
"View more comments on Reddit": "Prikaži več komentarjev na Reddit",
|
||||||
|
@ -201,22 +201,22 @@
|
||||||
"Yiddish": "jidiš",
|
"Yiddish": "jidiš",
|
||||||
"Yoruba": "joruba",
|
"Yoruba": "joruba",
|
||||||
"Xhosa": "xhosa",
|
"Xhosa": "xhosa",
|
||||||
"generic_count_years_0": "{{count}} leto",
|
"generic_count_years_0": "{{count}} letom",
|
||||||
"generic_count_years_1": "{{count}} leti",
|
"generic_count_years_1": "{{count}} leti",
|
||||||
"generic_count_years_2": "{{count}} leta",
|
"generic_count_years_2": "{{count}} leti",
|
||||||
"generic_count_years_3": "{{count}} let",
|
"generic_count_years_3": "{{count}} leti",
|
||||||
"generic_count_days_0": "{{count}} dan",
|
"generic_count_days_0": "{{count}} dnevom",
|
||||||
"generic_count_days_1": "{{count}} dneva",
|
"generic_count_days_1": "{{count}} dnevi",
|
||||||
"generic_count_days_2": "{{count}} dni",
|
"generic_count_days_2": "{{count}} dnevi",
|
||||||
"generic_count_days_3": "{{count}} dni",
|
"generic_count_days_3": "{{count}} dnevi",
|
||||||
"generic_count_hours_0": "{{count}} ura",
|
"generic_count_hours_0": "{{count}} uro",
|
||||||
"generic_count_hours_1": "{{count}} uri",
|
"generic_count_hours_1": "{{count}} urami",
|
||||||
"generic_count_hours_2": "{{count}} ure",
|
"generic_count_hours_2": "{{count}} urami",
|
||||||
"generic_count_hours_3": "{{count}} ur",
|
"generic_count_hours_3": "{{count}} urami",
|
||||||
"generic_count_minutes_0": "{{count}} minuta",
|
"generic_count_minutes_0": "{{count}} minuto",
|
||||||
"generic_count_minutes_1": "{{count}} minuti",
|
"generic_count_minutes_1": "{{count}} minutami",
|
||||||
"generic_count_minutes_2": "{{count}} minute",
|
"generic_count_minutes_2": "{{count}} minutami",
|
||||||
"generic_count_minutes_3": "{{count}} minut",
|
"generic_count_minutes_3": "{{count}} minutami",
|
||||||
"Search": "Iskanje",
|
"Search": "Iskanje",
|
||||||
"Top": "Vrh",
|
"Top": "Vrh",
|
||||||
"About": "O aplikaciji",
|
"About": "O aplikaciji",
|
||||||
|
@ -423,23 +423,23 @@
|
||||||
"Spanish (Spain)": "španščina (Španija)",
|
"Spanish (Spain)": "španščina (Španija)",
|
||||||
"Tajik": "tadžiščina",
|
"Tajik": "tadžiščina",
|
||||||
"Tamil": "tamilščina",
|
"Tamil": "tamilščina",
|
||||||
"generic_count_weeks_0": "{{count}} teden",
|
"generic_count_weeks_0": "{{count}} tednom",
|
||||||
"generic_count_weeks_1": "{{count}} tedna",
|
"generic_count_weeks_1": "{{count}} tedni",
|
||||||
"generic_count_weeks_2": "{{count}} tedne",
|
"generic_count_weeks_2": "{{count}} tedni",
|
||||||
"generic_count_weeks_3": "{{count}} tednov",
|
"generic_count_weeks_3": "{{count}} tedni",
|
||||||
"Swahili": "svahilščina",
|
"Swahili": "svahilščina",
|
||||||
"Swedish": "švedščina",
|
"Swedish": "švedščina",
|
||||||
"Vietnamese (auto-generated)": "vietnamščina (samodejno ustvarjeno)",
|
"Vietnamese (auto-generated)": "vietnamščina (samodejno ustvarjeno)",
|
||||||
"generic_count_months_0": "{{count}} mesec",
|
"generic_count_months_0": "{{count}} mesecem",
|
||||||
"generic_count_months_1": "{{count}} meseca",
|
"generic_count_months_1": "{{count}} meseci",
|
||||||
"generic_count_months_2": "{{count}} mesece",
|
"generic_count_months_2": "{{count}} meseci",
|
||||||
"generic_count_months_3": "{{count}} mesecev",
|
"generic_count_months_3": "{{count}} meseci",
|
||||||
"Uzbek": "uzbeščina",
|
"Uzbek": "uzbeščina",
|
||||||
"Zulu": "zulujščina",
|
"Zulu": "zulujščina",
|
||||||
"generic_count_seconds_0": "{{count}} sekunda",
|
"generic_count_seconds_0": "{{count}} sekundo",
|
||||||
"generic_count_seconds_1": "{{count}} sekundi",
|
"generic_count_seconds_1": "{{count}} sekundami",
|
||||||
"generic_count_seconds_2": "{{count}} sekunde",
|
"generic_count_seconds_2": "{{count}} sekundami",
|
||||||
"generic_count_seconds_3": "{{count}} sekund",
|
"generic_count_seconds_3": "{{count}} sekundami",
|
||||||
"Popular": "Priljubljeni",
|
"Popular": "Priljubljeni",
|
||||||
"Music": "Glasba",
|
"Music": "Glasba",
|
||||||
"Movies": "Filmi",
|
"Movies": "Filmi",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"LIVE": "ПРЯМИЙ ЕФІР",
|
"LIVE": "НАЖИВО",
|
||||||
"Shared `x` ago": "Розміщено `x` назад",
|
"Shared `x` ago": "Розміщено `x` тому",
|
||||||
"Unsubscribe": "Відписатися",
|
"Unsubscribe": "Відписатися",
|
||||||
"Subscribe": "Підписатися",
|
"Subscribe": "Підписатися",
|
||||||
"View channel on YouTube": "Подивитися канал на YouTube",
|
"View channel on YouTube": "Подивитися канал на YouTube",
|
||||||
|
@ -30,7 +30,7 @@
|
||||||
"Export subscriptions as OPML": "Експортувати підписки у форматі OPML",
|
"Export subscriptions as OPML": "Експортувати підписки у форматі OPML",
|
||||||
"Export subscriptions as OPML (for NewPipe & FreeTube)": "Експортувати підписки у форматі OPML (для NewPipe та FreeTube)",
|
"Export subscriptions as OPML (for NewPipe & FreeTube)": "Експортувати підписки у форматі OPML (для NewPipe та FreeTube)",
|
||||||
"Export data as JSON": "Експортувати дані Invidious у форматі JSON",
|
"Export data as JSON": "Експортувати дані Invidious у форматі JSON",
|
||||||
"Delete account?": "Видалити обліківку?",
|
"Delete account?": "Видалити обліковий запис?",
|
||||||
"History": "Історія",
|
"History": "Історія",
|
||||||
"An alternative front-end to YouTube": "Альтернативний фронтенд до YouTube",
|
"An alternative front-end to YouTube": "Альтернативний фронтенд до YouTube",
|
||||||
"JavaScript license information": "Інформація щодо ліцензій JavaScript",
|
"JavaScript license information": "Інформація щодо ліцензій JavaScript",
|
||||||
|
@ -40,9 +40,9 @@
|
||||||
"Log in with Google": "Увійти через Google",
|
"Log in with Google": "Увійти через Google",
|
||||||
"User ID": "ID користувача",
|
"User ID": "ID користувача",
|
||||||
"Password": "Пароль",
|
"Password": "Пароль",
|
||||||
"Time (h:mm:ss):": "Час (г:мм:сс):",
|
"Time (h:mm:ss):": "Час (г:хх:сс):",
|
||||||
"Text CAPTCHA": "Текст капчі",
|
"Text CAPTCHA": "Текст CAPTCHA",
|
||||||
"Image CAPTCHA": "Зображення капчі",
|
"Image CAPTCHA": "Зображення CAPTCHA",
|
||||||
"Sign In": "Увійти",
|
"Sign In": "Увійти",
|
||||||
"Register": "Зареєструватися",
|
"Register": "Зареєструватися",
|
||||||
"E-mail": "Електронна пошта",
|
"E-mail": "Електронна пошта",
|
||||||
|
@ -142,7 +142,7 @@
|
||||||
"Whitelisted regions: ": "Доступно у регіонах: ",
|
"Whitelisted regions: ": "Доступно у регіонах: ",
|
||||||
"Blacklisted regions: ": "Недоступно у регіонах: ",
|
"Blacklisted regions: ": "Недоступно у регіонах: ",
|
||||||
"Shared `x`": "Розміщено `x`",
|
"Shared `x`": "Розміщено `x`",
|
||||||
"Premieres in `x`": "Прем’єра через `x`",
|
"Premieres in `x`": "Прем’єра за `x`",
|
||||||
"Premieres `x`": "Прем’єра `x`",
|
"Premieres `x`": "Прем’єра `x`",
|
||||||
"Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Схоже, у вас відключений JavaScript. Щоб побачити коментарі, натисніть сюда, але майте на увазі, що вони можуть завантажуватися трохи довше.",
|
"Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Схоже, у вас відключений JavaScript. Щоб побачити коментарі, натисніть сюда, але майте на увазі, що вони можуть завантажуватися трохи довше.",
|
||||||
"View YouTube comments": "Переглянути коментарі з YouTube",
|
"View YouTube comments": "Переглянути коментарі з YouTube",
|
||||||
|
@ -157,11 +157,11 @@
|
||||||
"Incorrect password": "Неправильний пароль",
|
"Incorrect password": "Неправильний пароль",
|
||||||
"Quota exceeded, try again in a few hours": "Ліміт перевищено, спробуйте знову за декілька годин",
|
"Quota exceeded, try again in a few hours": "Ліміт перевищено, спробуйте знову за декілька годин",
|
||||||
"Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Не вдається увійти. Перевірте, чи не ввімкнена двофакторна аутентифікація (за кодом чи смс).",
|
"Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Не вдається увійти. Перевірте, чи не ввімкнена двофакторна аутентифікація (за кодом чи смс).",
|
||||||
"Invalid TFA code": "Неправильний код двофакторної аутентифікації",
|
"Invalid TFA code": "Неправильний код двофакторної автентифікації",
|
||||||
"Login failed. This may be because two-factor authentication is not turned on for your account.": "Не вдається увійти. Це може бути через те, що у вашій обліківці не ввімкнена двофакторна аутентифікація.",
|
"Login failed. This may be because two-factor authentication is not turned on for your account.": "Не вдається увійти. Це може бути через те, що у вашій обліківці не ввімкнена двофакторна аутентифікація.",
|
||||||
"Wrong answer": "Неправильна відповідь",
|
"Wrong answer": "Неправильна відповідь",
|
||||||
"Erroneous CAPTCHA": "Неправильна капча",
|
"Erroneous CAPTCHA": "Неправильна капча",
|
||||||
"CAPTCHA is a required field": "Необхідно пройти капчу",
|
"CAPTCHA is a required field": "Необхідно пройти CAPTCHA",
|
||||||
"User ID is a required field": "Необхідно ввести ID користувача",
|
"User ID is a required field": "Необхідно ввести ID користувача",
|
||||||
"Password is a required field": "Необхідно ввести пароль",
|
"Password is a required field": "Необхідно ввести пароль",
|
||||||
"Wrong username or password": "Неправильний логін чи пароль",
|
"Wrong username or password": "Неправильний логін чи пароль",
|
||||||
|
@ -169,7 +169,7 @@
|
||||||
"Password cannot be empty": "Пароль не може бути порожнім",
|
"Password cannot be empty": "Пароль не може бути порожнім",
|
||||||
"Password cannot be longer than 55 characters": "Пароль не може бути довшим за 55 знаків",
|
"Password cannot be longer than 55 characters": "Пароль не може бути довшим за 55 знаків",
|
||||||
"Please log in": "Будь ласка, увійдіть",
|
"Please log in": "Будь ласка, увійдіть",
|
||||||
"Invidious Private Feed for `x`": "Приватний поток відео Invidious для `x`",
|
"Invidious Private Feed for `x`": "Приватний потік відео Invidious для `x`",
|
||||||
"channel:`x`": "канал: `x`",
|
"channel:`x`": "канал: `x`",
|
||||||
"Deleted or invalid channel": "Канал видалено або не знайдено",
|
"Deleted or invalid channel": "Канал видалено або не знайдено",
|
||||||
"This channel does not exist.": "Такого каналу не існує.",
|
"This channel does not exist.": "Такого каналу не існує.",
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 020337194dd482c47ee2d53cd111d0ebf2831e52
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,109 @@
|
||||||
|
require "../parsers_helper.cr"
|
||||||
|
|
||||||
|
Spectator.describe Invidious::Hashtag do
|
||||||
|
it "parses richItemRenderer containers (test 1)" do
|
||||||
|
# Enable mock
|
||||||
|
test_content = load_mock("hashtag/martingarrix_page1")
|
||||||
|
videos = extract_items(test_content)
|
||||||
|
|
||||||
|
expect(typeof(videos)).to eq(Array(SearchItem))
|
||||||
|
expect(videos.size).to eq(60)
|
||||||
|
|
||||||
|
#
|
||||||
|
# Random video check 1
|
||||||
|
#
|
||||||
|
expect(typeof(videos[11])).to eq(SearchItem)
|
||||||
|
|
||||||
|
video_11 = videos[11].as(SearchVideo)
|
||||||
|
|
||||||
|
expect(video_11.id).to eq("06eSsOWcKYA")
|
||||||
|
expect(video_11.title).to eq("Martin Garrix - Live @ Tomorrowland 2018")
|
||||||
|
|
||||||
|
expect(video_11.ucid).to eq("UC5H_KXkPbEsGs0tFt8R35mA")
|
||||||
|
expect(video_11.author).to eq("Martin Garrix")
|
||||||
|
expect(video_11.author_verified).to be_true
|
||||||
|
|
||||||
|
expect(video_11.published).to be_close(Time.utc - 3.years, 1.second)
|
||||||
|
expect(video_11.length_seconds).to eq((56.minutes + 41.seconds).total_seconds.to_i32)
|
||||||
|
expect(video_11.views).to eq(40_504_893)
|
||||||
|
|
||||||
|
expect(video_11.live_now).to be_false
|
||||||
|
expect(video_11.premium).to be_false
|
||||||
|
expect(video_11.premiere_timestamp).to be_nil
|
||||||
|
|
||||||
|
#
|
||||||
|
# Random video check 2
|
||||||
|
#
|
||||||
|
expect(typeof(videos[35])).to eq(SearchItem)
|
||||||
|
|
||||||
|
video_35 = videos[35].as(SearchVideo)
|
||||||
|
|
||||||
|
expect(video_35.id).to eq("b9HpOAYjY9I")
|
||||||
|
expect(video_35.title).to eq("Martin Garrix feat. Mike Yung - Dreamer (Official Video)")
|
||||||
|
|
||||||
|
expect(video_35.ucid).to eq("UC5H_KXkPbEsGs0tFt8R35mA")
|
||||||
|
expect(video_35.author).to eq("Martin Garrix")
|
||||||
|
expect(video_35.author_verified).to be_true
|
||||||
|
|
||||||
|
expect(video_35.published).to be_close(Time.utc - 3.years, 1.second)
|
||||||
|
expect(video_35.length_seconds).to eq((3.minutes + 14.seconds).total_seconds.to_i32)
|
||||||
|
expect(video_35.views).to eq(30_790_049)
|
||||||
|
|
||||||
|
expect(video_35.live_now).to be_false
|
||||||
|
expect(video_35.premium).to be_false
|
||||||
|
expect(video_35.premiere_timestamp).to be_nil
|
||||||
|
end
|
||||||
|
|
||||||
|
it "parses richItemRenderer containers (test 2)" do
|
||||||
|
# Enable mock
|
||||||
|
test_content = load_mock("hashtag/martingarrix_page2")
|
||||||
|
videos = extract_items(test_content)
|
||||||
|
|
||||||
|
expect(typeof(videos)).to eq(Array(SearchItem))
|
||||||
|
expect(videos.size).to eq(60)
|
||||||
|
|
||||||
|
#
|
||||||
|
# Random video check 1
|
||||||
|
#
|
||||||
|
expect(typeof(videos[41])).to eq(SearchItem)
|
||||||
|
|
||||||
|
video_41 = videos[41].as(SearchVideo)
|
||||||
|
|
||||||
|
expect(video_41.id).to eq("qhstH17zAjs")
|
||||||
|
expect(video_41.title).to eq("Martin Garrix Radio - Episode 391")
|
||||||
|
|
||||||
|
expect(video_41.ucid).to eq("UC5H_KXkPbEsGs0tFt8R35mA")
|
||||||
|
expect(video_41.author).to eq("Martin Garrix")
|
||||||
|
expect(video_41.author_verified).to be_true
|
||||||
|
|
||||||
|
expect(video_41.published).to be_close(Time.utc - 2.months, 1.second)
|
||||||
|
expect(video_41.length_seconds).to eq((1.hour).total_seconds.to_i32)
|
||||||
|
expect(video_41.views).to eq(63_240)
|
||||||
|
|
||||||
|
expect(video_41.live_now).to be_false
|
||||||
|
expect(video_41.premium).to be_false
|
||||||
|
expect(video_41.premiere_timestamp).to be_nil
|
||||||
|
|
||||||
|
#
|
||||||
|
# Random video check 2
|
||||||
|
#
|
||||||
|
expect(typeof(videos[48])).to eq(SearchItem)
|
||||||
|
|
||||||
|
video_48 = videos[48].as(SearchVideo)
|
||||||
|
|
||||||
|
expect(video_48.id).to eq("lqGvW0NIfdc")
|
||||||
|
expect(video_48.title).to eq("Martin Garrix SENTIO Full Album Mix by Sakul")
|
||||||
|
|
||||||
|
expect(video_48.ucid).to eq("UC3833PXeLTS6yRpwGMQpp4Q")
|
||||||
|
expect(video_48.author).to eq("SAKUL")
|
||||||
|
expect(video_48.author_verified).to be_false
|
||||||
|
|
||||||
|
expect(video_48.published).to be_close(Time.utc - 3.weeks, 1.second)
|
||||||
|
expect(video_48.length_seconds).to eq((35.minutes + 46.seconds).total_seconds.to_i32)
|
||||||
|
expect(video_48.views).to eq(68_704)
|
||||||
|
|
||||||
|
expect(video_48.live_now).to be_false
|
||||||
|
expect(video_48.premium).to be_false
|
||||||
|
expect(video_48.premiere_timestamp).to be_nil
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,33 @@
|
||||||
|
require "db"
|
||||||
|
require "json"
|
||||||
|
require "kemal"
|
||||||
|
|
||||||
|
require "protodec/utils"
|
||||||
|
|
||||||
|
require "spectator"
|
||||||
|
|
||||||
|
require "../src/invidious/helpers/macros"
|
||||||
|
require "../src/invidious/helpers/logger"
|
||||||
|
require "../src/invidious/helpers/utils"
|
||||||
|
|
||||||
|
require "../src/invidious/videos"
|
||||||
|
require "../src/invidious/comments"
|
||||||
|
|
||||||
|
require "../src/invidious/helpers/serialized_yt_data"
|
||||||
|
require "../src/invidious/yt_backend/extractors"
|
||||||
|
require "../src/invidious/yt_backend/extractors_utils"
|
||||||
|
|
||||||
|
OUTPUT = File.open(File::NULL, "w")
|
||||||
|
LOGGER = Invidious::LogHandler.new(OUTPUT, LogLevel::Off)
|
||||||
|
|
||||||
|
def load_mock(file) : Hash(String, JSON::Any)
|
||||||
|
file = File.join(__DIR__, "..", "mocks", file + ".json")
|
||||||
|
content = File.read(file)
|
||||||
|
|
||||||
|
return JSON.parse(content).as_h
|
||||||
|
end
|
||||||
|
|
||||||
|
Spectator.configure do |config|
|
||||||
|
config.fail_blank
|
||||||
|
config.randomize
|
||||||
|
end
|
|
@ -385,6 +385,7 @@ end
|
||||||
Invidious::Routing.get "/opensearch.xml", Invidious::Routes::Search, :opensearch
|
Invidious::Routing.get "/opensearch.xml", Invidious::Routes::Search, :opensearch
|
||||||
Invidious::Routing.get "/results", Invidious::Routes::Search, :results
|
Invidious::Routing.get "/results", Invidious::Routes::Search, :results
|
||||||
Invidious::Routing.get "/search", Invidious::Routes::Search, :search
|
Invidious::Routing.get "/search", Invidious::Routes::Search, :search
|
||||||
|
Invidious::Routing.get "/hashtag/:hashtag", Invidious::Routes::Search, :hashtag
|
||||||
|
|
||||||
# User routes
|
# User routes
|
||||||
define_user_routes()
|
define_user_routes()
|
||||||
|
|
|
@ -61,6 +61,7 @@ def get_about_info(ucid, locale) : AboutChannel
|
||||||
author = initdata["metadata"]["channelMetadataRenderer"]["title"].as_s
|
author = initdata["metadata"]["channelMetadataRenderer"]["title"].as_s
|
||||||
author_url = initdata["metadata"]["channelMetadataRenderer"]["channelUrl"].as_s
|
author_url = initdata["metadata"]["channelMetadataRenderer"]["channelUrl"].as_s
|
||||||
author_thumbnail = initdata["metadata"]["channelMetadataRenderer"]["avatar"]["thumbnails"][0]["url"].as_s
|
author_thumbnail = initdata["metadata"]["channelMetadataRenderer"]["avatar"]["thumbnails"][0]["url"].as_s
|
||||||
|
author_verified = has_verified_badge?(initdata.dig?("header", "c4TabbedHeaderRenderer", "badges"))
|
||||||
|
|
||||||
ucid = initdata["metadata"]["channelMetadataRenderer"]["externalId"].as_s
|
ucid = initdata["metadata"]["channelMetadataRenderer"]["externalId"].as_s
|
||||||
|
|
||||||
|
@ -71,9 +72,6 @@ def get_about_info(ucid, locale) : AboutChannel
|
||||||
# if banner.includes? "channels/c4/default_banner"
|
# if banner.includes? "channels/c4/default_banner"
|
||||||
# banner = nil
|
# banner = nil
|
||||||
# end
|
# end
|
||||||
# author_verified_badges = initdata["header"]?.try &.["c4TabbedHeaderRenderer"]?.try &.["badges"]?
|
|
||||||
author_verified_badge = initdata["header"].dig?("c4TabbedHeaderRenderer", "badges", 0, "metadataBadgeRenderer", "tooltip")
|
|
||||||
author_verified = (author_verified_badge && author_verified_badge == "Verified")
|
|
||||||
|
|
||||||
description_node = initdata["metadata"]["channelMetadataRenderer"]?.try &.["description"]?
|
description_node = initdata["metadata"]["channelMetadataRenderer"]?.try &.["description"]?
|
||||||
|
|
||||||
|
|
|
@ -481,7 +481,7 @@ def template_reddit_comments(root, locale)
|
||||||
|
|
||||||
html << <<-END_HTML
|
html << <<-END_HTML
|
||||||
<p>
|
<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>
|
<b><a href="https://www.reddit.com/user/#{child.author}">#{child.author}</a></b>
|
||||||
#{translate_count(locale, "comments_points_count", child.score, NumberFormatting::Separator)}
|
#{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>
|
<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>
|
||||||
|
@ -500,6 +500,12 @@ def template_reddit_comments(root, locale)
|
||||||
end
|
end
|
||||||
|
|
||||||
def replace_links(html)
|
def replace_links(html)
|
||||||
|
# Check if the document is empty
|
||||||
|
# Prevents edge-case bug with Reddit comments, see issue #3115
|
||||||
|
if html.nil? || html.empty?
|
||||||
|
return html
|
||||||
|
end
|
||||||
|
|
||||||
html = XML.parse_html(html)
|
html = XML.parse_html(html)
|
||||||
|
|
||||||
html.xpath_nodes(%q(//a)).each do |anchor|
|
html.xpath_nodes(%q(//a)).each do |anchor|
|
||||||
|
@ -541,6 +547,12 @@ def replace_links(html)
|
||||||
end
|
end
|
||||||
|
|
||||||
def fill_links(html, scheme, host)
|
def fill_links(html, scheme, host)
|
||||||
|
# Check if the document is empty
|
||||||
|
# Prevents edge-case bug with Reddit comments, see issue #3115
|
||||||
|
if html.nil? || html.empty?
|
||||||
|
return html
|
||||||
|
end
|
||||||
|
|
||||||
html = XML.parse_html(html)
|
html = XML.parse_html(html)
|
||||||
|
|
||||||
html.xpath_nodes("//a").each do |match|
|
html.xpath_nodes("//a").each do |match|
|
||||||
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
module Invidious::Hashtag
|
||||||
|
extend self
|
||||||
|
|
||||||
|
def fetch(hashtag : String, page : Int, region : String? = nil) : Array(SearchItem)
|
||||||
|
cursor = (page - 1) * 60
|
||||||
|
ctoken = generate_continuation(hashtag, cursor)
|
||||||
|
|
||||||
|
client_config = YoutubeAPI::ClientConfig.new(region: region)
|
||||||
|
response = YoutubeAPI.browse(continuation: ctoken, client_config: client_config)
|
||||||
|
|
||||||
|
return extract_items(response)
|
||||||
|
end
|
||||||
|
|
||||||
|
def generate_continuation(hashtag : String, cursor : Int)
|
||||||
|
object = {
|
||||||
|
"80226972:embedded" => {
|
||||||
|
"2:string" => "FEhashtag",
|
||||||
|
"3:base64" => {
|
||||||
|
"1:varint" => cursor.to_i64,
|
||||||
|
},
|
||||||
|
"7:base64" => {
|
||||||
|
"325477796:embedded" => {
|
||||||
|
"1:embedded" => {
|
||||||
|
"2:0:embedded" => {
|
||||||
|
"2:string" => '#' + hashtag,
|
||||||
|
"4:varint" => 0_i64,
|
||||||
|
"11:string" => "",
|
||||||
|
},
|
||||||
|
"4:string" => "browse-feedFEhashtag",
|
||||||
|
},
|
||||||
|
"2:string" => hashtag,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
continuation = object.try { |i| Protodec::Any.cast_json(i) }
|
||||||
|
.try { |i| Protodec::Any.from_json(i) }
|
||||||
|
.try { |i| Base64.urlsafe_encode(i) }
|
||||||
|
.try { |i| URI.encode_www_form(i) }
|
||||||
|
|
||||||
|
return continuation
|
||||||
|
end
|
||||||
|
end
|
|
@ -182,7 +182,7 @@ module Invidious::Routes::Feeds
|
||||||
paid: false,
|
paid: false,
|
||||||
premium: false,
|
premium: false,
|
||||||
premiere_timestamp: nil,
|
premiere_timestamp: nil,
|
||||||
author_verified: false, # ¯\_(ツ)_/¯
|
author_verified: false,
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -63,4 +63,35 @@ module Invidious::Routes::Search
|
||||||
templated "search"
|
templated "search"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.hashtag(env : HTTP::Server::Context)
|
||||||
|
locale = env.get("preferences").as(Preferences).locale
|
||||||
|
|
||||||
|
hashtag = env.params.url["hashtag"]?
|
||||||
|
if hashtag.nil? || hashtag.empty?
|
||||||
|
return error_template(400, "Invalid request")
|
||||||
|
end
|
||||||
|
|
||||||
|
page = env.params.query["page"]?
|
||||||
|
if page.nil?
|
||||||
|
page = 1
|
||||||
|
else
|
||||||
|
page = Math.max(1, page.to_i)
|
||||||
|
env.params.query.delete_all("page")
|
||||||
|
end
|
||||||
|
|
||||||
|
begin
|
||||||
|
videos = Invidious::Hashtag.fetch(hashtag, page)
|
||||||
|
rescue ex
|
||||||
|
return error_template(500, ex)
|
||||||
|
end
|
||||||
|
|
||||||
|
params = env.params.query.empty? ? "" : "&#{env.params.query}"
|
||||||
|
|
||||||
|
hashtag_encoded = URI.encode_www_form(hashtag, space_to_plus: false)
|
||||||
|
url_prev_page = "/hashtag/#{hashtag_encoded}?page=#{page - 1}#{params}"
|
||||||
|
url_next_page = "/hashtag/#{hashtag_encoded}?page=#{page + 1}#{params}"
|
||||||
|
|
||||||
|
templated "hashtag"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -853,6 +853,7 @@ end
|
||||||
# the same 11 first entries as the compact rendered.
|
# the same 11 first entries as the compact rendered.
|
||||||
#
|
#
|
||||||
# TODO: "compactRadioRenderer" (Mix) and
|
# TODO: "compactRadioRenderer" (Mix) and
|
||||||
|
# TODO: Use a proper struct/class instead of a hacky JSON object
|
||||||
def parse_related_video(related : JSON::Any) : Hash(String, JSON::Any)?
|
def parse_related_video(related : JSON::Any) : Hash(String, JSON::Any)?
|
||||||
return nil if !related["videoId"]?
|
return nil if !related["videoId"]?
|
||||||
|
|
||||||
|
@ -868,11 +869,7 @@ def parse_related_video(related : JSON::Any) : Hash(String, JSON::Any)?
|
||||||
.try &.dig?("runs", 0)
|
.try &.dig?("runs", 0)
|
||||||
|
|
||||||
author = channel_info.try &.dig?("text")
|
author = channel_info.try &.dig?("text")
|
||||||
author_verified_badge = related["ownerBadges"]?.try do |badges_array|
|
author_verified = has_verified_badge?(related["ownerBadges"]?).to_s
|
||||||
badges_array.as_a.find(&.dig("metadataBadgeRenderer", "tooltip").as_s.== "Verified")
|
|
||||||
end
|
|
||||||
|
|
||||||
author_verified = (author_verified_badge && author_verified_badge.size > 0).to_s
|
|
||||||
|
|
||||||
ucid = channel_info.try { |ci| HelperExtractors.get_browse_id(ci) }
|
ucid = channel_info.try { |ci| HelperExtractors.get_browse_id(ci) }
|
||||||
|
|
||||||
|
@ -1089,17 +1086,19 @@ def extract_video_info(video_id : String, proxy_region : String? = nil, context_
|
||||||
|
|
||||||
# Author infos
|
# Author infos
|
||||||
|
|
||||||
author_info = video_secondary_renderer.try &.dig?("owner", "videoOwnerRenderer")
|
if author_info = video_secondary_renderer.try &.dig?("owner", "videoOwnerRenderer")
|
||||||
author_thumbnail = author_info.try &.dig?("thumbnail", "thumbnails", 0, "url")
|
author_thumbnail = author_info.dig?("thumbnail", "thumbnails", 0, "url")
|
||||||
|
params["authorThumbnail"] = JSON::Any.new(author_thumbnail.try &.as_s || "")
|
||||||
|
|
||||||
author_verified_badge = author_info.try &.dig?("badges", 0, "metadataBadgeRenderer", "tooltip")
|
author_verified = has_verified_badge?(author_info["badges"]?)
|
||||||
author_verified = (!author_verified_badge.nil? && author_verified_badge == "Verified")
|
params["authorVerified"] = JSON::Any.new(author_verified)
|
||||||
params["authorVerified"] = JSON::Any.new(author_verified)
|
|
||||||
|
|
||||||
params["authorThumbnail"] = JSON::Any.new(author_thumbnail.try &.as_s || "")
|
subs_text = author_info["subscriberCountText"]?
|
||||||
|
.try { |t| t["simpleText"]? || t.dig?("runs", 0, "text") }
|
||||||
|
.try &.as_s.split(" ", 2)[0]
|
||||||
|
|
||||||
params["subCountText"] = JSON::Any.new(author_info.try &.["subscriberCountText"]?
|
params["subCountText"] = JSON::Any.new(subs_text || "-")
|
||||||
.try { |t| t["simpleText"]? || t.dig?("runs", 0, "text") }.try &.as_s.split(" ", 2)[0] || "-")
|
end
|
||||||
|
|
||||||
# Return data
|
# Return data
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
<% content_for "header" do %>
|
||||||
|
<title><%= HTML.escape(hashtag) %> - Invidious</title>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<hr/>
|
||||||
|
|
||||||
|
<div class="pure-g h-box v-box">
|
||||||
|
<div class="pure-u-1 pure-u-lg-1-5">
|
||||||
|
<%- if page > 1 -%>
|
||||||
|
<a href="<%= url_prev_page %>"><%= translate(locale, "Previous page") %></a>
|
||||||
|
<%- end -%>
|
||||||
|
</div>
|
||||||
|
<div class="pure-u-1 pure-u-lg-3-5"></div>
|
||||||
|
<div class="pure-u-1 pure-u-lg-1-5" style="text-align:right">
|
||||||
|
<%- if videos.size >= 60 -%>
|
||||||
|
<a href="<%= url_next_page %>"><%= translate(locale, "Next page") %></a>
|
||||||
|
<%- end -%>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pure-g">
|
||||||
|
<%- videos.each do |item| -%>
|
||||||
|
<%= rendered "components/item" %>
|
||||||
|
<%- end -%>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pure-g h-box">
|
||||||
|
<div class="pure-u-1 pure-u-lg-1-5">
|
||||||
|
<%- if page > 1 -%>
|
||||||
|
<a href="<%= url_prev_page %>"><%= translate(locale, "Previous page") %></a>
|
||||||
|
<%- end -%>
|
||||||
|
</div>
|
||||||
|
<div class="pure-u-1 pure-u-lg-3-5"></div>
|
||||||
|
<div class="pure-u-1 pure-u-lg-1-5" style="text-align:right">
|
||||||
|
<%- if videos.size >= 60 -%>
|
||||||
|
<a href="<%= url_next_page %>"><%= translate(locale, "Next page") %></a>
|
||||||
|
<%- end -%>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -9,6 +9,20 @@
|
||||||
<body>
|
<body>
|
||||||
<h1><%= translate(locale, "JavaScript license information") %></h1>
|
<h1><%= translate(locale, "JavaScript license information") %></h1>
|
||||||
<table id="jslicense-labels1">
|
<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>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<a href="/js/community.js?v=<%= ASSET_COMMIT %>">community.js</a>
|
<a href="/js/community.js?v=<%= ASSET_COMMIT %>">community.js</a>
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
<link rel="stylesheet" href="/css/ionicons.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 %>">
|
<link rel="stylesheet" href="/css/default.css?v=<%= ASSET_COMMIT %>">
|
||||||
<link rel="stylesheet" href="/css/keromod.css?v=<%= ASSET_COMMIT %>">
|
<link rel="stylesheet" href="/css/keromod.css?v=<%= ASSET_COMMIT %>">
|
||||||
|
<script src="/js/_helpers.js?v=<%= ASSET_COMMIT %>"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<%
|
<%
|
||||||
|
|
|
@ -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="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="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: ") %>
|
<p id="genre"><%= translate(locale, "Genre: ") %>
|
||||||
<% if !video.genre_url %>
|
<% if !video.genre_url %>
|
||||||
<%= video.genre %>
|
<%= video.genre %>
|
||||||
|
@ -186,7 +186,7 @@ we're going to need to do it here in order to allow for translations.
|
||||||
<% end %>
|
<% end %>
|
||||||
<p id="family_friendly"><%= translate(locale, "Family friendly? ") %><%= translate_bool(locale, video.is_family_friendly) %></p>
|
<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="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>
|
<p id="engagement"><%= translate(locale, "Engagement: ") %><%= video.engagement %>%</p>
|
||||||
<% if video.allowed_regions.size != REGIONS.size %>
|
<% if video.allowed_regions.size != REGIONS.size %>
|
||||||
<p id="allowed_regions">
|
<p id="allowed_regions">
|
||||||
|
@ -278,24 +278,24 @@ we're going to need to do it here in order to allow for translations.
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<p style="width:100%"><%= rv["title"] %></p>
|
<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" %> <i class="icon ion ion-md-checkmark-circle"></i><% end %></a></b>
|
|
||||||
<% else %>
|
|
||||||
<b style="width:100%"><%= rv["author"]? %><% if rv["author_verified"]? == "true" %> <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>
|
</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" %> <i class="icon ion ion-md-checkmark-circle"></i><% end %></a></b>
|
||||||
|
<% else %>
|
||||||
|
<b style="width:100%"><%= rv["author"]? %><% if rv["author_verified"]? == "true" %> <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 %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
require "../helpers/serialized_yt_data"
|
||||||
|
|
||||||
# This file contains helper methods to parse the Youtube API json data into
|
# This file contains helper methods to parse the Youtube API json data into
|
||||||
# neat little packages we can use
|
# neat little packages we can use
|
||||||
|
|
||||||
|
@ -14,6 +16,7 @@ private ITEM_PARSERS = {
|
||||||
Parsers::GridPlaylistRendererParser,
|
Parsers::GridPlaylistRendererParser,
|
||||||
Parsers::PlaylistRendererParser,
|
Parsers::PlaylistRendererParser,
|
||||||
Parsers::CategoryRendererParser,
|
Parsers::CategoryRendererParser,
|
||||||
|
Parsers::RichItemRendererParser,
|
||||||
}
|
}
|
||||||
|
|
||||||
record AuthorFallback, name : String, id : String
|
record AuthorFallback, name : String, id : String
|
||||||
|
@ -57,6 +60,8 @@ private module Parsers
|
||||||
author_id = author_fallback.id
|
author_id = author_fallback.id
|
||||||
end
|
end
|
||||||
|
|
||||||
|
author_verified = has_verified_badge?(item_contents["ownerBadges"]?)
|
||||||
|
|
||||||
# For live videos (and possibly recently premiered videos) there is no published information.
|
# For live videos (and possibly recently premiered videos) there is no published information.
|
||||||
# Instead, in its place is the amount of people currently watching. This behavior should be replicated
|
# Instead, in its place is the amount of people currently watching. This behavior should be replicated
|
||||||
# on Invidious once all features of livestreams are supported. On an unrelated note, defaulting to the current
|
# on Invidious once all features of livestreams are supported. On an unrelated note, defaulting to the current
|
||||||
|
@ -102,11 +107,7 @@ private module Parsers
|
||||||
premium = false
|
premium = false
|
||||||
|
|
||||||
premiere_timestamp = item_contents.dig?("upcomingEventData", "startTime").try { |t| Time.unix(t.as_s.to_i64) }
|
premiere_timestamp = item_contents.dig?("upcomingEventData", "startTime").try { |t| Time.unix(t.as_s.to_i64) }
|
||||||
author_verified_badge = item_contents["ownerBadges"]?.try do |badges_array|
|
|
||||||
badges_array.as_a.find(&.dig("metadataBadgeRenderer", "tooltip").as_s.== "Verified")
|
|
||||||
end
|
|
||||||
|
|
||||||
author_verified = (author_verified_badge && author_verified_badge.size > 0)
|
|
||||||
item_contents["badges"]?.try &.as_a.each do |badge|
|
item_contents["badges"]?.try &.as_a.each do |badge|
|
||||||
b = badge["metadataBadgeRenderer"]
|
b = badge["metadataBadgeRenderer"]
|
||||||
case b["label"].as_s
|
case b["label"].as_s
|
||||||
|
@ -133,7 +134,7 @@ private module Parsers
|
||||||
live_now: live_now,
|
live_now: live_now,
|
||||||
premium: premium,
|
premium: premium,
|
||||||
premiere_timestamp: premiere_timestamp,
|
premiere_timestamp: premiere_timestamp,
|
||||||
author_verified: author_verified || false,
|
author_verified: author_verified,
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -161,12 +162,9 @@ private module Parsers
|
||||||
private def self.parse(item_contents, author_fallback)
|
private def self.parse(item_contents, author_fallback)
|
||||||
author = extract_text(item_contents["title"]) || author_fallback.name
|
author = extract_text(item_contents["title"]) || author_fallback.name
|
||||||
author_id = item_contents["channelId"]?.try &.as_s || author_fallback.id
|
author_id = item_contents["channelId"]?.try &.as_s || author_fallback.id
|
||||||
author_verified_badge = item_contents["ownerBadges"]?.try do |badges_array|
|
author_verified = has_verified_badge?(item_contents["ownerBadges"]?)
|
||||||
badges_array.as_a.find(&.dig("metadataBadgeRenderer", "tooltip").as_s.== "Verified")
|
|
||||||
end
|
|
||||||
|
|
||||||
author_verified = (author_verified_badge && author_verified_badge.size > 0)
|
|
||||||
author_thumbnail = HelperExtractors.get_thumbnails(item_contents)
|
author_thumbnail = HelperExtractors.get_thumbnails(item_contents)
|
||||||
|
|
||||||
# When public subscriber count is disabled, the subscriberCountText isn't sent by InnerTube.
|
# When public subscriber count is disabled, the subscriberCountText isn't sent by InnerTube.
|
||||||
# Always simpleText
|
# Always simpleText
|
||||||
# TODO change default value to nil
|
# TODO change default value to nil
|
||||||
|
@ -188,7 +186,7 @@ private module Parsers
|
||||||
video_count: video_count,
|
video_count: video_count,
|
||||||
description_html: description_html,
|
description_html: description_html,
|
||||||
auto_generated: auto_generated,
|
auto_generated: auto_generated,
|
||||||
author_verified: author_verified || false,
|
author_verified: author_verified,
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -216,11 +214,9 @@ private module Parsers
|
||||||
private def self.parse(item_contents, author_fallback)
|
private def self.parse(item_contents, author_fallback)
|
||||||
title = extract_text(item_contents["title"]) || ""
|
title = extract_text(item_contents["title"]) || ""
|
||||||
plid = item_contents["playlistId"]?.try &.as_s || ""
|
plid = item_contents["playlistId"]?.try &.as_s || ""
|
||||||
author_verified_badge = item_contents["ownerBadges"]?.try do |badges_array|
|
|
||||||
badges_array.as_a.find(&.dig("metadataBadgeRenderer", "tooltip").as_s.== "Verified")
|
|
||||||
end
|
|
||||||
|
|
||||||
author_verified = (author_verified_badge && author_verified_badge.size > 0)
|
author_verified = has_verified_badge?(item_contents["ownerBadges"]?)
|
||||||
|
|
||||||
video_count = HelperExtractors.get_video_count(item_contents)
|
video_count = HelperExtractors.get_video_count(item_contents)
|
||||||
playlist_thumbnail = HelperExtractors.get_thumbnails(item_contents)
|
playlist_thumbnail = HelperExtractors.get_thumbnails(item_contents)
|
||||||
|
|
||||||
|
@ -232,7 +228,7 @@ private module Parsers
|
||||||
video_count: video_count,
|
video_count: video_count,
|
||||||
videos: [] of SearchPlaylistVideo,
|
videos: [] of SearchPlaylistVideo,
|
||||||
thumbnail: playlist_thumbnail,
|
thumbnail: playlist_thumbnail,
|
||||||
author_verified: author_verified || false,
|
author_verified: author_verified,
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -266,11 +262,8 @@ private module Parsers
|
||||||
author_info = item_contents.dig?("shortBylineText", "runs", 0)
|
author_info = item_contents.dig?("shortBylineText", "runs", 0)
|
||||||
author = author_info.try &.["text"].as_s || author_fallback.name
|
author = author_info.try &.["text"].as_s || author_fallback.name
|
||||||
author_id = author_info.try { |x| HelperExtractors.get_browse_id(x) } || author_fallback.id
|
author_id = author_info.try { |x| HelperExtractors.get_browse_id(x) } || author_fallback.id
|
||||||
author_verified_badge = item_contents["ownerBadges"]?.try do |badges_array|
|
author_verified = has_verified_badge?(item_contents["ownerBadges"]?)
|
||||||
badges_array.as_a.find(&.dig("metadataBadgeRenderer", "tooltip").as_s.== "Verified")
|
|
||||||
end
|
|
||||||
|
|
||||||
author_verified = (author_verified_badge && author_verified_badge.size > 0)
|
|
||||||
videos = item_contents["videos"]?.try &.as_a.map do |v|
|
videos = item_contents["videos"]?.try &.as_a.map do |v|
|
||||||
v = v["childVideoRenderer"]
|
v = v["childVideoRenderer"]
|
||||||
v_title = v.dig?("title", "simpleText").try &.as_s || ""
|
v_title = v.dig?("title", "simpleText").try &.as_s || ""
|
||||||
|
@ -293,7 +286,7 @@ private module Parsers
|
||||||
video_count: video_count,
|
video_count: video_count,
|
||||||
videos: videos,
|
videos: videos,
|
||||||
thumbnail: playlist_thumbnail,
|
thumbnail: playlist_thumbnail,
|
||||||
author_verified: author_verified || false,
|
author_verified: author_verified,
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -374,6 +367,29 @@ private module Parsers
|
||||||
return {{@type.name}}
|
return {{@type.name}}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Parses an InnerTube richItemRenderer into a SearchVideo.
|
||||||
|
# Returns nil when the given object isn't a shelfRenderer
|
||||||
|
#
|
||||||
|
# A richItemRenderer seems to be a simple wrapper for a videoRenderer, used
|
||||||
|
# by the result page for hashtags. It is located inside a continuationItems
|
||||||
|
# container.
|
||||||
|
#
|
||||||
|
module RichItemRendererParser
|
||||||
|
def self.process(item : JSON::Any, author_fallback : AuthorFallback)
|
||||||
|
if item_contents = item.dig?("richItemRenderer", "content")
|
||||||
|
return self.parse(item_contents, author_fallback)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private def self.parse(item_contents, author_fallback)
|
||||||
|
return VideoRendererParser.process(item_contents, author_fallback)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.parser_name
|
||||||
|
return {{@type.name}}
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# The following are the extractors for extracting an array of items from
|
# The following are the extractors for extracting an array of items from
|
||||||
|
@ -501,6 +517,8 @@ private module Extractors
|
||||||
self.extract(target)
|
self.extract(target)
|
||||||
elsif target = initial_data["appendContinuationItemsAction"]?
|
elsif target = initial_data["appendContinuationItemsAction"]?
|
||||||
self.extract(target)
|
self.extract(target)
|
||||||
|
elsif target = initial_data["reloadContinuationItemsCommand"]?
|
||||||
|
self.extract(target)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -29,6 +29,45 @@ def extract_text(item : JSON::Any?) : String?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Check if an "ownerBadges" or a "badges" element contains a verified badge.
|
||||||
|
# There is currently two known types of verified badges:
|
||||||
|
#
|
||||||
|
# "ownerBadges": [{
|
||||||
|
# "metadataBadgeRenderer": {
|
||||||
|
# "icon": { "iconType": "CHECK_CIRCLE_THICK" },
|
||||||
|
# "style": "BADGE_STYLE_TYPE_VERIFIED",
|
||||||
|
# "tooltip": "Verified",
|
||||||
|
# "accessibilityData": { "label": "Verified" }
|
||||||
|
# }
|
||||||
|
# }],
|
||||||
|
#
|
||||||
|
# "ownerBadges": [{
|
||||||
|
# "metadataBadgeRenderer": {
|
||||||
|
# "icon": { "iconType": "OFFICIAL_ARTIST_BADGE" },
|
||||||
|
# "style": "BADGE_STYLE_TYPE_VERIFIED_ARTIST",
|
||||||
|
# "tooltip": "Official Artist Channel",
|
||||||
|
# "accessibilityData": { "label": "Official Artist Channel" }
|
||||||
|
# }
|
||||||
|
# }],
|
||||||
|
#
|
||||||
|
def has_verified_badge?(badges : JSON::Any?)
|
||||||
|
return false if badges.nil?
|
||||||
|
|
||||||
|
badges.as_a.each do |badge|
|
||||||
|
style = badge.dig("metadataBadgeRenderer", "style").as_s
|
||||||
|
|
||||||
|
return true if style == "BADGE_STYLE_TYPE_VERIFIED"
|
||||||
|
return true if style == "BADGE_STYLE_TYPE_VERIFIED_ARTIST"
|
||||||
|
end
|
||||||
|
|
||||||
|
return false
|
||||||
|
rescue ex
|
||||||
|
LOGGER.debug("Unable to parse owner badges. Got exception: #{ex.message}")
|
||||||
|
LOGGER.trace("Owner badges data: #{badges.to_json}")
|
||||||
|
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
def extract_videos(initial_data : Hash(String, JSON::Any), author_fallback : String? = nil, author_id_fallback : String? = nil)
|
def extract_videos(initial_data : Hash(String, JSON::Any), author_fallback : String? = nil, author_id_fallback : String? = nil)
|
||||||
extracted = extract_items(initial_data, author_fallback, author_id_fallback)
|
extracted = extract_items(initial_data, author_fallback, author_id_fallback)
|
||||||
|
|
||||||
|
|
読み込み中…
新しいイシューから参照