Compare commits
No commits in common. "master" and "v0.0.7" have entirely different histories.
@ -1,28 +0,0 @@
|
|||||||
# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.224.3/containers/python-3/.devcontainer/base.Dockerfile
|
|
||||||
|
|
||||||
# [Choice] Python version (use -bullseye variants on local arm64/Apple Silicon): 3, 3.10, 3.9, 3.8, 3.7, 3.6, 3-bullseye, 3.10-bullseye, 3.9-bullseye, 3.8-bullseye, 3.7-bullseye, 3.6-bullseye, 3-buster, 3.10-buster, 3.9-buster, 3.8-buster, 3.7-buster, 3.6-buster
|
|
||||||
ARG VARIANT="3.10-bullseye"
|
|
||||||
FROM mcr.microsoft.com/vscode/devcontainers/python:0-${VARIANT}
|
|
||||||
|
|
||||||
# [Choice] Node.js version: none, lts/*, 16, 14, 12, 10
|
|
||||||
ARG NODE_VERSION="none"
|
|
||||||
RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi
|
|
||||||
|
|
||||||
RUN sed -i 's/required/sufficient/g' /etc/pam.d/chsh
|
|
||||||
|
|
||||||
# [Optional] If your pip requirements rarely change, uncomment this section to add them to the image.
|
|
||||||
COPY tubearchivist/requirements.txt /tmp/pip-tmp/
|
|
||||||
RUN pip3 --disable-pip-version-check --no-cache-dir install -r /tmp/pip-tmp/requirements.txt \
|
|
||||||
&& rm -rf /tmp/pip-tmp
|
|
||||||
|
|
||||||
# [Optional] Uncomment this section to install additional OS packages.
|
|
||||||
RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
|
|
||||||
&& apt-get -y install --no-install-recommends fish
|
|
||||||
|
|
||||||
ENV SHELL /usr/bin/fish
|
|
||||||
|
|
||||||
USER vscode
|
|
||||||
RUN fish -c "curl -sL https://git.io/fisher | source && fisher install jorgebucaran/fisher"
|
|
||||||
|
|
||||||
# [Optional] Uncomment this line to install global node packages.
|
|
||||||
# RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g <your-package-here>" 2>&1
|
|
@ -1,64 +0,0 @@
|
|||||||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
|
|
||||||
// https://github.com/microsoft/vscode-dev-containers/tree/v0.224.3/containers/python-3
|
|
||||||
{
|
|
||||||
"name": "Python 3",
|
|
||||||
"build": {
|
|
||||||
"dockerfile": "Dockerfile",
|
|
||||||
"context": "..",
|
|
||||||
"args": {
|
|
||||||
// Update 'VARIANT' to pick a Python version: 3, 3.10, 3.9, 3.8, 3.7, 3.6
|
|
||||||
// Append -bullseye or -buster to pin to an OS version.
|
|
||||||
// Use -bullseye variants on local on arm64/Apple Silicon.
|
|
||||||
"VARIANT": "3.10-bullseye",
|
|
||||||
// Options
|
|
||||||
"NODE_VERSION": "16"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Set *default* container specific settings.json values on container create.
|
|
||||||
"settings": {
|
|
||||||
"python.defaultInterpreterPath": "/usr/local/bin/python",
|
|
||||||
"python.linting.enabled": true,
|
|
||||||
"python.linting.pylintEnabled": true,
|
|
||||||
"python.formatting.autopep8Path": "/usr/local/py-utils/bin/autopep8",
|
|
||||||
"python.formatting.blackPath": "/usr/local/py-utils/bin/black",
|
|
||||||
"python.formatting.yapfPath": "/usr/local/py-utils/bin/yapf",
|
|
||||||
"python.linting.banditPath": "/usr/local/py-utils/bin/bandit",
|
|
||||||
"python.linting.flake8Path": "/usr/local/py-utils/bin/flake8",
|
|
||||||
"python.linting.mypyPath": "/usr/local/py-utils/bin/mypy",
|
|
||||||
"python.linting.pycodestylePath": "/usr/local/py-utils/bin/pycodestyle",
|
|
||||||
"python.linting.pydocstylePath": "/usr/local/py-utils/bin/pydocstyle",
|
|
||||||
"python.linting.pylintPath": "/usr/local/py-utils/bin/pylint",
|
|
||||||
"typescript.tsdk": "tubearchivist/www/node_modules/typescript/lib",
|
|
||||||
"terminal.integrated.defaultProfile.linux": "fish"
|
|
||||||
},
|
|
||||||
|
|
||||||
// Add the IDs of extensions you want installed when the container is created.
|
|
||||||
"extensions": [
|
|
||||||
"ms-python.python",
|
|
||||||
"ms-python.vscode-pylance",
|
|
||||||
"esbenp.prettier-vscode",
|
|
||||||
"dbaeumer.vscode-eslint",
|
|
||||||
"eamodio.gitlens",
|
|
||||||
"batisteo.vscode-django",
|
|
||||||
"christian-kohler.path-intellisense",
|
|
||||||
"quicktype.quicktype"
|
|
||||||
],
|
|
||||||
|
|
||||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
|
||||||
"forwardPorts": [3000, 8000],
|
|
||||||
|
|
||||||
// Use 'postCreateCommand' to run commands after the container is created.
|
|
||||||
"postCreateCommand": "chsh -s /usr/bin/fish && fish -c 'fisher install matchai/spacefish'",
|
|
||||||
// Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
|
|
||||||
"remoteUser": "vscode",
|
|
||||||
"features": {
|
|
||||||
// "fish": "latest",
|
|
||||||
"github-cli": "latest",
|
|
||||||
"docker-in-docker": {
|
|
||||||
"version": "latest",
|
|
||||||
"moby": true
|
|
||||||
},
|
|
||||||
"git": "os-provided"
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,8 +0,0 @@
|
|||||||
# https://next-auth.js.org/configuration/options#nextauth_secret Used to encrypt JWT
|
|
||||||
NEXTAUTH_SECRET=
|
|
||||||
|
|
||||||
# https://next-auth.js.org/configuration/options#nextauth_url When deploying to production, set the NEXTAUTH_URL environment variable to the canonical URL of your site.
|
|
||||||
NEXTAUTH_URL=
|
|
||||||
|
|
||||||
# URL of the Tubearchivist server without a trailing /
|
|
||||||
NEXT_PUBLIC_TUBEARCHIVIST_URL=
|
|
@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "next/core-web-vitals"
|
|
||||||
}
|
|
27
.github/workflows/ci.yml
vendored
@ -1,27 +0,0 @@
|
|||||||
name: Node.js CI
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [master]
|
|
||||||
pull_request:
|
|
||||||
branches: [master]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
node-version: [14.x, 16.x]
|
|
||||||
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- name: Use Node.js ${{ matrix.node-version }}
|
|
||||||
uses: actions/setup-node@v3
|
|
||||||
with:
|
|
||||||
node-version: ${{ matrix.node-version }}
|
|
||||||
cache: "yarn"
|
|
||||||
- run: yarn install --frozen-lockfile
|
|
||||||
- run: yarn build
|
|
||||||
- run: yarn lint
|
|
23
.github/workflows/lint_python.yml
vendored
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
name: lint_python
|
||||||
|
on: [pull_request, push]
|
||||||
|
jobs:
|
||||||
|
lint_python:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions/setup-python@v2
|
||||||
|
- run: pip install --upgrade pip wheel
|
||||||
|
- run: pip install bandit black codespell flake8 flake8-bugbear
|
||||||
|
flake8-comprehensions isort
|
||||||
|
- run: bandit --recursive --skip B105,B108,B404,B603,B607 .
|
||||||
|
- run: black --check --diff --line-length 79 .
|
||||||
|
- run: codespell
|
||||||
|
- run: flake8 . --count --max-complexity=12 --max-line-length=79
|
||||||
|
--show-source --statistics
|
||||||
|
- run: isort --check-only --line-length 79 --profile black .
|
||||||
|
# - run: pip install -r tubearchivist/requirements.txt
|
||||||
|
# - run: mkdir --parents --verbose .mypy_cache
|
||||||
|
# - run: mypy --ignore-missing-imports --install-types --non-interactive .
|
||||||
|
# - run: python3 tubearchivist/manage.py test || true
|
||||||
|
# - run: shopt -s globstar && pyupgrade --py36-plus **/*.py || true
|
||||||
|
# - run: safety check
|
38
.gitignore
vendored
@ -1,35 +1,5 @@
|
|||||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
# python testing cache
|
||||||
|
__pycache__
|
||||||
|
|
||||||
# dependencies
|
# django testing db
|
||||||
/node_modules
|
db.sqlite3
|
||||||
/.pnp
|
|
||||||
.pnp.js
|
|
||||||
|
|
||||||
# testing
|
|
||||||
/coverage
|
|
||||||
|
|
||||||
# next.js
|
|
||||||
/.next/
|
|
||||||
/out/
|
|
||||||
|
|
||||||
# production
|
|
||||||
/build
|
|
||||||
|
|
||||||
# misc
|
|
||||||
.DS_Store
|
|
||||||
*.pem
|
|
||||||
|
|
||||||
# debug
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
.pnpm-debug.log*
|
|
||||||
|
|
||||||
# local env files
|
|
||||||
.env*.local
|
|
||||||
|
|
||||||
# vercel
|
|
||||||
.vercel
|
|
||||||
|
|
||||||
# typescript
|
|
||||||
*.tsbuildinfo
|
|
||||||
|
1
.husky/.gitignore
vendored
@ -1 +0,0 @@
|
|||||||
_
|
|
@ -1,4 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
. "$(dirname "$0")/_/husky.sh"
|
|
||||||
|
|
||||||
npx lint-staged
|
|
10
.prettierrc
@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"printWidth": 100,
|
|
||||||
"tabWidth": 2,
|
|
||||||
"useTabs": false,
|
|
||||||
"semi": true,
|
|
||||||
"singleQuote": false,
|
|
||||||
"trailingComma": "all",
|
|
||||||
"bracketSpacing": true,
|
|
||||||
"arrowParens": "always"
|
|
||||||
}
|
|
5
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"python.linting.pylintEnabled": true,
|
||||||
|
"python.linting.pycodestyleEnabled": false,
|
||||||
|
"python.linting.enabled": true
|
||||||
|
}
|
@ -5,7 +5,7 @@ If you haven't already, the best place to start is the README. This will give yo
|
|||||||
|
|
||||||
## Report a bug
|
## Report a bug
|
||||||
|
|
||||||
If you notice something is not working as expected, check to see if it has been previously reported in the [open issues](https://github.com/tubearchivist/tubearchivist/issues).
|
If you notice something is not working as expected, check to see if it has been previously reported in the [open issues](https://github.com/bbilly1/tubearchivist/issues).
|
||||||
If it has not yet been disclosed, go ahead and create an issue.
|
If it has not yet been disclosed, go ahead and create an issue.
|
||||||
If the issue doesn't move forward due to a lack of response, I assume it's solved and will close it after some time to keep the list fresh.
|
If the issue doesn't move forward due to a lack of response, I assume it's solved and will close it after some time to keep the list fresh.
|
||||||
|
|
||||||
@ -20,12 +20,12 @@ I have learned the hard way, that working on a dockerized application outside of
|
|||||||
This is my setup I have landed on, YMMV:
|
This is my setup I have landed on, YMMV:
|
||||||
- Clone the repo, work on it with your favorite code editor in your local filesystem. *testing* branch is the where all the changes are happening, might be unstable and is WIP.
|
- Clone the repo, work on it with your favorite code editor in your local filesystem. *testing* branch is the where all the changes are happening, might be unstable and is WIP.
|
||||||
- Then I have a VM on KVM hypervisor running standard Ubuntu Server LTS with docker installed. The VM keeps my projects separate and offers convenient snapshot functionality. The VM also offers ways to simulate lowend environments by limiting CPU cores and memory. But you could also just run docker on your host system.
|
- Then I have a VM on KVM hypervisor running standard Ubuntu Server LTS with docker installed. The VM keeps my projects separate and offers convenient snapshot functionality. The VM also offers ways to simulate lowend environments by limiting CPU cores and memory. But you could also just run docker on your host system.
|
||||||
- The `Dockerfile` is structured in a way that the actual application code is in the last layer so rebuilding the image with only code changes utilizes the build cache for everything else and will just take a few seconds.
|
- The `Dockerfile` is structured in a way that the actual application code is in the last layer so rebuilding the image with only code changes utilizes the build cache for everything else and will take just 2-3 secs.
|
||||||
- Take a look at the `deploy.sh` file. I have my local DNS resolve `tubearchivist.local` to the IP of the VM for convenience. To deploy the latest changes and rebuild the application to the testing VM run:
|
- Take a look at the `deploy.sh` file. I have my local DNS resolve `tubearchivist.local` to the IP of the VM for convenience. To deploy the latest changes and rebuild the application to the testing VM run:
|
||||||
```bash
|
```bash
|
||||||
./deploy.sh test
|
./deploy.sh test
|
||||||
```
|
```
|
||||||
- The command above will call the docker build command with `--build-arg INSTALL_DEBUG=1` to install additional useful debug tools.
|
- The command above will also copy the file `tubarchivist/testing.sh` into the working folder of the container. Running this script will install additional debugging tools I regularly use in testing.
|
||||||
- The `test` argument takes another optional argument to build for a specific architecture valid options are: `amd64`, `arm64` and `multi`, default is `amd64`.
|
- The `test` argument takes another optional argument to build for a specific architecture valid options are: `amd64`, `arm64` and `multi`, default is `amd64`.
|
||||||
- This `deploy.sh` file is not meant to be universally usable for every possible environment but could serve as an idea on how to automatically rebuild containers to test changes - customize to your liking.
|
- This `deploy.sh` file is not meant to be universally usable for every possible environment but could serve as an idea on how to automatically rebuild containers to test changes - customize to your liking.
|
||||||
|
|
||||||
@ -44,12 +44,7 @@ To fix a bug or implement a feature, fork the repository and make all changes to
|
|||||||
|
|
||||||
## Releases
|
## Releases
|
||||||
|
|
||||||
There are three different docker tags:
|
Everything on the master branch is what's in the latest release and is what you get in your container when you `pull` either the *:latest* tag or the newest named version. If you want to test the newest changes and improvements, clone the repository and build the docker container with the Dockerfile from the testing branch.
|
||||||
- **latest**: As the name implies is the latest multiarch release for regular usage.
|
|
||||||
- **unstable**: Intermediate amd64 builds for quick testing and improved collaboration. Don't mix with a *latest* installation, for your testing environment only. This is untested and WIP and will have breaking changes between commits that might require a reset to resolve.
|
|
||||||
- **semantic versioning**: There will be a handful named version tags that will also have a matching release and tag on github.
|
|
||||||
|
|
||||||
If you want to see what's in your container, checkout the matching release tag. A merge to **master** usually means a *latest* or *unstable* release. If you want to preview changes in your testing environment, pull the *unstable* tag or clone the repository and build the docker container with the Dockerfile from the **testing** branch.
|
|
||||||
|
|
||||||
## Code formatting and linting
|
## Code formatting and linting
|
||||||
|
|
||||||
|
59
Dockerfile
@ -1,19 +1,21 @@
|
|||||||
# multi stage to build tube archivist
|
# build the tube archivist image from default python slim image
|
||||||
# first stage to build python wheel, copy into final image
|
|
||||||
|
|
||||||
|
FROM python:3.9.7-slim-bullseye
|
||||||
# First stage to build python wheel
|
|
||||||
FROM python:3.10.4-slim-bullseye AS builder
|
|
||||||
ARG TARGETPLATFORM
|
ARG TARGETPLATFORM
|
||||||
|
|
||||||
RUN apt-get update
|
ENV PYTHONUNBUFFERED 1
|
||||||
RUN apt-get install -y --no-install-recommends build-essential gcc curl
|
|
||||||
|
# install distro packages needed
|
||||||
|
RUN apt-get clean && apt-get -y update && apt-get -y install --no-install-recommends \
|
||||||
|
build-essential \
|
||||||
|
nginx \
|
||||||
|
curl && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# get newest patched ffmpeg and ffprobe builds for amd64 fall back to repo ffmpeg for arm64
|
# get newest patched ffmpeg and ffprobe builds for amd64 fall back to repo ffmpeg for arm64
|
||||||
RUN if [ "$TARGETPLATFORM" = "linux/amd64" ] ; then \
|
RUN if [ "$TARGETPLATFORM" = "linux/amd64" ] ; then \
|
||||||
curl -s https://api.github.com/repos/yt-dlp/FFmpeg-Builds/releases/latest \
|
curl -s https://api.github.com/repos/yt-dlp/FFmpeg-Builds/releases/latest \
|
||||||
| grep browser_download_url \
|
| grep browser_download_url \
|
||||||
| grep ".*master.*linux64.*tar.xz" \
|
| grep linux64-gpl-4.4.tar.xz \
|
||||||
| cut -d '"' -f 4 \
|
| cut -d '"' -f 4 \
|
||||||
| xargs curl -L --output ffmpeg.tar.xz && \
|
| xargs curl -L --output ffmpeg.tar.xz && \
|
||||||
tar -xf ffmpeg.tar.xz --strip-components=2 --no-anchored -C /usr/bin/ "ffmpeg" && \
|
tar -xf ffmpeg.tar.xz --strip-components=2 --no-anchored -C /usr/bin/ "ffmpeg" && \
|
||||||
@ -23,49 +25,22 @@ RUN if [ "$TARGETPLATFORM" = "linux/amd64" ] ; then \
|
|||||||
apt-get -y update && apt-get -y install --no-install-recommends ffmpeg && rm -rf /var/lib/apt/lists/* \
|
apt-get -y update && apt-get -y install --no-install-recommends ffmpeg && rm -rf /var/lib/apt/lists/* \
|
||||||
; fi
|
; fi
|
||||||
|
|
||||||
# install requirements
|
# copy config files
|
||||||
COPY ./tubearchivist/requirements.txt /requirements.txt
|
COPY nginx.conf /etc/nginx/conf.d/
|
||||||
RUN pip install --user -r requirements.txt
|
|
||||||
|
|
||||||
# build final image
|
|
||||||
FROM python:3.10.4-slim-bullseye as tubearchivist
|
|
||||||
|
|
||||||
ARG TARGETPLATFORM
|
|
||||||
ARG INSTALL_DEBUG
|
|
||||||
|
|
||||||
ENV PYTHONUNBUFFERED 1
|
|
||||||
|
|
||||||
# copy build requirements
|
|
||||||
COPY --from=builder /root/.local /root/.local
|
|
||||||
COPY --from=builder /usr/bin/ffmpeg /usr/bin/ffmpeg
|
|
||||||
COPY --from=builder /usr/bin/ffprobe /usr/bin/ffprobe
|
|
||||||
ENV PATH=/root/.local/bin:$PATH
|
|
||||||
|
|
||||||
# install distro packages needed
|
|
||||||
RUN apt-get clean && apt-get -y update && apt-get -y install --no-install-recommends \
|
|
||||||
nginx \
|
|
||||||
atomicparsley \
|
|
||||||
curl && rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# install debug tools for testing environment
|
|
||||||
RUN if [ "$INSTALL_DEBUG" ] ; then \
|
|
||||||
apt-get -y update && apt-get -y install --no-install-recommends \
|
|
||||||
vim htop bmon net-tools iputils-ping procps \
|
|
||||||
&& pip install --user ipython \
|
|
||||||
; fi
|
|
||||||
|
|
||||||
# make folders
|
# make folders
|
||||||
RUN mkdir /cache
|
RUN mkdir /cache
|
||||||
RUN mkdir /youtube
|
RUN mkdir /youtube
|
||||||
RUN mkdir /app
|
RUN mkdir /app
|
||||||
|
|
||||||
# copy config files
|
# install python dependencies
|
||||||
COPY docker_assets/nginx.conf /etc/nginx/sites-available/default
|
COPY ./tubearchivist/requirements.txt /requirements.txt
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt --src /usr/local/src
|
||||||
|
|
||||||
# copy application into container
|
# copy application into container
|
||||||
COPY ./tubearchivist /app
|
COPY ./tubearchivist /app
|
||||||
COPY ./docker_assets/run.sh /app
|
COPY ./run.sh /app
|
||||||
COPY ./docker_assets/uwsgi.ini /app
|
COPY ./uwsgi.ini /app
|
||||||
|
|
||||||
# volumes
|
# volumes
|
||||||
VOLUME /cache
|
VOLUME /cache
|
||||||
|
183
README.md
@ -1,51 +1,158 @@
|
|||||||
# Tube Archivist Frontend
|
![Tube Archivist](assets/tube-archivist-banner.jpg?raw=true "Tube Archivist Banner")
|
||||||
|
|
||||||
This repo is WIP, recreation of [Tube Archivist](https://github.com/tubearchivist/tubearchivist) frontend in NextJS/React.
|
<center><h1>Your self hosted YouTube media server</h1></center>
|
||||||
|
|
||||||
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
|
## Table of contents:
|
||||||
|
* [Wiki](https://github.com/bbilly1/tubearchivist/wiki) for a detailed documentation
|
||||||
|
* [Core functionality](#core-functionality)
|
||||||
|
* [Screenshots](#screenshots)
|
||||||
|
* [Problem Tube Archivist tries to solve](#problem-tube-archivist-tries-to-solve)
|
||||||
|
* [Installing and updating](#installing-and-updating)
|
||||||
|
* [Getting Started](#getting-started)
|
||||||
|
* [Potential pitfalls](#potential-pitfalls)
|
||||||
|
* [Roadmap](#roadmap)
|
||||||
|
* [Known limitations](#known-limitations)
|
||||||
|
* [Donate](#donate)
|
||||||
|
|
||||||
## Setup your environment
|
------------------------
|
||||||
Copy *.env.local.example* to *.env.local* and set:
|
|
||||||
- **NEXTAUTH_SECRET**: Some long random string
|
|
||||||
- **NEXTAUTH_URL**: Your frontend, most likely `http://localhost:3000`
|
|
||||||
- **NEXT_PUBLIC_TUBEARCHIVIST_URL**: Your Tube Archivist backend testing server, e.g. `http://localhost:8000`
|
|
||||||
|
|
||||||
In general: Use the [unstable builds](https://github.com/tubearchivist/tubearchivist/blob/master/CONTRIBUTING.md#releases) from Tube Archivist or build the image yourself from *testing* branch.
|
## Core functionality
|
||||||
|
* Subscribe to your favorite YouTube channels
|
||||||
|
* Download Videos using **yt-dlp**
|
||||||
|
* Index and make videos searchable
|
||||||
|
* Play videos
|
||||||
|
* Keep track of viewed and unviewed videos
|
||||||
|
|
||||||
|
## Screenshots
|
||||||
|
![home screenshot](assets/tube-archivist-screenshot-home.png?raw=true "Tube Archivist Home")
|
||||||
|
*Home Page*
|
||||||
|
|
||||||
|
![channels screenshot](assets/tube-archivist-screenshot-channels.png?raw=true "Tube Archivist Channels")
|
||||||
|
*All Channels*
|
||||||
|
|
||||||
|
![single channel screenshot](assets/tube-archivist-screenshot-single-channel.png?raw=true "Tube Archivist Single Channel")
|
||||||
|
*Single Channel*
|
||||||
|
|
||||||
|
![video page screenshot](assets/tube-archivist-screenshot-video.png?raw=true "Tube Archivist Video Page")
|
||||||
|
*Video Page*
|
||||||
|
|
||||||
|
![video page screenshot](assets/tube-archivist-screenshot-download.png?raw=true "Tube Archivist Video Page")
|
||||||
|
*Downloads Page*
|
||||||
|
|
||||||
|
## Problem Tube Archivist tries to solve
|
||||||
|
Once your YouTube video collection grows, it becomes hard to search and find a specific video. That's where Tube Archivist comes in: By indexing your video collection with metadata from YouTube, you can organize, search and enjoy your archived YouTube videos without hassle offline through a convenient web interface.
|
||||||
|
|
||||||
|
## Installing and updating
|
||||||
|
Take a look at the example `docker-compose.yml` file provided. Tube Archivist depends on three main components split up into separate docker containers:
|
||||||
|
|
||||||
|
### Tube Archivist
|
||||||
|
The main Python application that displays and serves your video collection, built with Django.
|
||||||
|
- Serves the interface on port `8000`
|
||||||
|
- Needs a volume for the video archive at **/youtube**
|
||||||
|
- And another volume to save application data at **/cache**.
|
||||||
|
- The environment variables `ES_URL` and `REDIS_HOST` are needed to tell Tube Archivist where Elasticsearch and Redis respectively are located.
|
||||||
|
- The environment variables `HOST_UID` and `HOST_GID` allows Tube Archivist to `chown` the video files to the main host system user instead of the container user. Those two variables are optional, not setting them will disable that functionality. That might be needed if the underlying filesystem doesn't support `chown` like *NFS*.
|
||||||
|
- Change the environment variables `TA_USERNAME` and `TA_PASSWORD` to create the initial credentials.
|
||||||
|
- `ELASTIC_PASSWORD` is for the password for Elasticsearch. The environment variable `ELASTIC_USER` is optional, should you want to change the username from the default *elastic*.
|
||||||
|
|
||||||
|
### Elasticsearch
|
||||||
|
Stores video meta data and makes everything searchable. Also keeps track of the download queue.
|
||||||
|
- Needs to be accessible over the default port `9200`
|
||||||
|
- Needs a volume at **/usr/share/elasticsearch/data** to store data
|
||||||
|
|
||||||
|
Follow the [documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/docker.html) for additional installation details.
|
||||||
|
|
||||||
|
### Redis JSON
|
||||||
|
Functions as a cache and temporary link between the application and the file system. Used to store and display messages and configuration variables.
|
||||||
|
- Needs to be accessible over the default port `6379`
|
||||||
|
- Needs a volume at **/data** to make your configuration changes permanent.
|
||||||
|
|
||||||
|
### Redis on a custom port
|
||||||
|
For some architectures it might be required to run Redis JSON on a nonstandard port. To for example change the Redis port to **6380**, set the following values:
|
||||||
|
- Set the environment variable `REDIS_PORT=6380` to the *tubearchivist* service.
|
||||||
|
- For the *archivist-redis* service, change the ports to `6380:6380`
|
||||||
|
- Additionally set the following value to the *archivist-redis* service: `command: --port 6380 --loadmodule /usr/lib/redis/modules/rejson.so`
|
||||||
|
|
||||||
|
### Updating Tube Archivist
|
||||||
|
You will see the current version number of **Tube Archivist** in the footer of the interface so you can compare it with the latest release to make sure you are running the *latest and greatest*.
|
||||||
|
* There can be breaking changes between updates, particularly as the application grows, new environment variables or settings might be required for you to set in the your docker-compose file. *Always* check the **release notes**: Any breaking changes will be marked there.
|
||||||
|
* All testing and development is done with the Elasticsearch version number as mentioned in the provided *docker-compose.yml* file. This will be updated when a new release of Elasticsearch is available. Running an older version of Elasticsearch is most likely not going to result in any issues, but it's still recommended to run the same version as mentioned.
|
||||||
|
|
||||||
|
### Alternative installation instructions:
|
||||||
|
- **arm64**: Newest Tube Archivist container is multi arch, so is Elasticsearch. RedisJSON doesn't offer arm builds, you can use `bbilly1/rejson`, an unofficial rebuild for arm64.
|
||||||
|
- NOTE: This is untested, looking for feedback.
|
||||||
|
- **Synology**: There is a [discussion thread](https://github.com/bbilly1/tubearchivist/discussions/48) with Synology installation instructions.
|
||||||
|
- **Unraid**: The three containers needed are all in the Community Applications. First install `TubeArchivist RedisJSON` followed by `TubeArchivist ES`, and finally you can install `TubeArchivist`. If you have unraid specific issues, report those to the [support thread](https://forums.unraid.net/topic/114073-support-crocs-tube-archivist/ "support thread").
|
||||||
|
|
||||||
|
|
||||||
|
## Potential pitfalls
|
||||||
|
### vm.max_map_count
|
||||||
|
**Elastic Search** in Docker requires the kernel setting of the host machine `vm.max_map_count` to be set to at least 262144.
|
||||||
|
|
||||||
|
To temporary set the value run:
|
||||||
|
```
|
||||||
|
sudo sysctl -w vm.max_map_count=262144
|
||||||
|
```
|
||||||
|
|
||||||
|
To apply the change permanently depends on your host operating system:
|
||||||
|
- For example on Ubuntu Server add `vm.max_map_count = 262144` to the file */etc/sysctl.conf*.
|
||||||
|
- On Arch based systems create a file */etc/sysctl.d/max_map_count.conf* with the content `vm.max_map_count = 262144`.
|
||||||
|
- On any other platform look up in the documentation on how to pass kernel parameters.
|
||||||
|
|
||||||
|
### Permissions for elasticsearch
|
||||||
|
If you see a message similar to `AccessDeniedException[/usr/share/elasticsearch/data/nodes]` when initially starting elasticsearch, that means the container is not allowed to write files to the volume.
|
||||||
|
That's most likely the case when you run `docker-compose` as an unprivileged user. To fix that issue, shutdown the container and on your host machine run:
|
||||||
|
```
|
||||||
|
chown 1000:0 /path/to/mount/point
|
||||||
|
```
|
||||||
|
This will match the permissions with the **UID** and **GID** of elasticsearch within the container and should fix the issue.
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
1. Go through the **settings** page and look at the available options. Particularly set *Download Format* to your desired video quality before downloading. **Tube Archivist** downloads the best available quality by default. To support iOS or MacOS a compatible format must be specified. For example:
|
||||||
First, run the development server:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run dev
|
|
||||||
# or
|
|
||||||
yarn dev
|
|
||||||
```
|
```
|
||||||
|
bestvideo[VCODEC=avc1]+bestaudio[ACODEC=mp4a]/mp4
|
||||||
|
```
|
||||||
|
2. Subscribe to some of your favorite YouTube channels on the **channels** page.
|
||||||
|
3. On the **downloads** page, click on *Rescan subscriptions* to add videos from the subscribed channels to your Download queue or click on *Add to download queue* to manually add Video IDs, links, channels or playlists.
|
||||||
|
4. Click on *Start download* and let **Tube Archivist** to it's thing.
|
||||||
|
5. Enjoy your archived collection!
|
||||||
|
|
||||||
|
## Roadmap
|
||||||
|
This should be considered as a **minimal viable product**, there is an extensive list of future functions and improvements planned.
|
||||||
|
|
||||||
### Errors:
|
### Functionality
|
||||||
- *next command not found*: Install next with `npm install next`
|
- [ ] User roles
|
||||||
- *Error: Invalid src prop [...] hostname [...] is not configured under images in your `next.config.js`*: Add the *NEXT_PUBLIC_TUBEARCHIVIST_URL* to the list of *domains*.
|
- [ ] Create playlists
|
||||||
- *CORS errors in console*: Set the environment variable `DISABLE_CORS=True` to the Tube Archivist container to circumvent this protection. NEVER do that on network accessible installation.
|
- [ ] Podcast mode to serve channel as mp3
|
||||||
|
- [ ] Implement [PyFilesystem](https://github.com/PyFilesystem/pyfilesystem2) for flexible video storage
|
||||||
|
- [ ] Optional automatic deletion of watched items after a specified time
|
||||||
|
- [ ] Subtitle download & indexing
|
||||||
|
- [X] Access control [2021-11-01]
|
||||||
|
- [X] Delete videos and channel [2021-10-16]
|
||||||
|
- [X] Add thumbnail embed option [2021-10-16]
|
||||||
|
- [X] Un-ignore videos [2021-10-03]
|
||||||
|
- [X] Dynamic download queue [2021-09-26]
|
||||||
|
- [X] Backup and restore [2021-09-22]
|
||||||
|
- [X] Scan your file system to index already downloaded videos [2021-09-14]
|
||||||
|
|
||||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
### UI
|
||||||
|
- [ ] Show similar videos on video page
|
||||||
|
- [ ] Multi language support
|
||||||
|
- [ ] Show total video downloaded vs total videos available in channel
|
||||||
|
- [X] Grid and list view for both channel and video list pages [2021-10-03]
|
||||||
|
- [X] Create a github wiki for user documentation [2021-10-03]
|
||||||
|
|
||||||
You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file.
|
|
||||||
|
|
||||||
[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`.
|
## Known limitations
|
||||||
|
- Video files created by Tube Archivist need to be **mp4** video files for best browser compatibility.
|
||||||
|
- Every limitation of **yt-dlp** will also be present in Tube Archivist. If **yt-dlp** can't download or extract a video for any reason, Tube Archivist won't be able to either.
|
||||||
|
- For now this is meant to be run in a trusted network environment. Not everything is properly authenticated.
|
||||||
|
- There is currently no flexibility in naming of the media files.
|
||||||
|
|
||||||
The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.
|
|
||||||
|
|
||||||
## Learn More
|
## Donate
|
||||||
|
The best donation to **Tube Archivist** is your time, take a look at the [contribution page](CONTRIBUTING.md) to get started.
|
||||||
To learn more about Next.js, take a look at the following resources:
|
Second best way to support the development is to provide for caffeinated beverages:
|
||||||
|
* [Paypal.me](https://paypal.me/bbilly1) for a one time coffee
|
||||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
* [Paypal Subscription](https://www.paypal.com/webapps/billing/plans/subscribe?plan_id=P-03770005GR991451KMFGVPMQ) for a monthly coffee
|
||||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
* [co-fi.com](https://ko-fi.com/bbilly1) for an alternative platform
|
||||||
|
|
||||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
|
|
||||||
|
|
||||||
## Deploy on Vercel
|
|
||||||
|
|
||||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
|
||||||
|
|
||||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
|
|
||||||
|
Before Width: | Height: | Size: 113 KiB |
Before Width: | Height: | Size: 131 KiB After Width: | Height: | Size: 103 KiB |
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 72 KiB |
Before Width: | Height: | Size: 174 KiB After Width: | Height: | Size: 138 KiB |
Before Width: | Height: | Size: 166 KiB After Width: | Height: | Size: 156 KiB |
Before Width: | Height: | Size: 238 KiB After Width: | Height: | Size: 81 KiB |
161
deploy.sh
Executable file
@ -0,0 +1,161 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# deploy all needed project files to different servers:
|
||||||
|
# test for local vm for testing
|
||||||
|
# blackhole for local production
|
||||||
|
# docker to publish
|
||||||
|
|
||||||
|
# create builder:
|
||||||
|
# docker buildx create --name tubearchivist
|
||||||
|
# docker buildx use tubearchivist
|
||||||
|
# docker buildx inspect --bootstrap
|
||||||
|
|
||||||
|
# more details:
|
||||||
|
# https://github.com/bbilly1/tubearchivist/issues/6
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
function sync_blackhole {
|
||||||
|
|
||||||
|
# docker commands need sudo, only build amd64
|
||||||
|
host="blackhole.local"
|
||||||
|
|
||||||
|
read -sp 'Password: ' remote_pw
|
||||||
|
export PASS=$remote_pw
|
||||||
|
|
||||||
|
rsync -a --progress --delete-after \
|
||||||
|
--exclude ".git" \
|
||||||
|
--exclude ".gitignore" \
|
||||||
|
--exclude "**/cache" \
|
||||||
|
--exclude "**/__pycache__/" \
|
||||||
|
--exclude "db.sqlite3" \
|
||||||
|
. -e ssh "$host":tubearchivist
|
||||||
|
|
||||||
|
echo "$PASS" | ssh "$host" 'sudo -S docker buildx build --platform linux/amd64 -t bbilly1/tubearchivist:latest tubearchivist --load 2>/dev/null'
|
||||||
|
echo "$PASS" | ssh "$host" 'sudo -S docker-compose up -d 2>/dev/null'
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function sync_test {
|
||||||
|
|
||||||
|
# docker commands don't need sudo in testing vm
|
||||||
|
# pass argument to build for specific platform
|
||||||
|
|
||||||
|
host="tubearchivist.local"
|
||||||
|
|
||||||
|
rsync -a --progress --delete-after \
|
||||||
|
--exclude ".git" \
|
||||||
|
--exclude ".gitignore" \
|
||||||
|
--exclude "**/cache" \
|
||||||
|
--exclude "**/__pycache__/" \
|
||||||
|
--exclude "db.sqlite3" \
|
||||||
|
. -e ssh "$host":tubearchivist
|
||||||
|
|
||||||
|
rsync -r --progress --delete docker-compose.yml -e ssh "$host":docker
|
||||||
|
|
||||||
|
if [[ $1 = "amd64" ]]; then
|
||||||
|
platform="linux/amd64"
|
||||||
|
elif [[ $1 = "arm64" ]]; then
|
||||||
|
platform="linux/arm64"
|
||||||
|
elif [[ $1 = "multi" ]]; then
|
||||||
|
platform="linux/amd64,linux/arm64"
|
||||||
|
else
|
||||||
|
platform="linux/amd64"
|
||||||
|
fi
|
||||||
|
|
||||||
|
ssh "$host" "docker buildx build --platform $platform -t bbilly1/tubearchivist:latest tubearchivist --load"
|
||||||
|
ssh "$host" 'docker-compose -f docker/docker-compose.yml up -d'
|
||||||
|
|
||||||
|
ssh "$host" 'docker cp tubearchivist/tubearchivist/testing.sh tubearchivist:/app/testing.sh'
|
||||||
|
ssh "$host" 'docker exec tubearchivist chmod +x /app/testing.sh'
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# run same tests and checks as with github action but locally
|
||||||
|
# takes filename to validate as optional argument
|
||||||
|
function validate {
|
||||||
|
|
||||||
|
if [[ $1 ]]; then
|
||||||
|
check_path="$1"
|
||||||
|
else
|
||||||
|
check_path="."
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "run validate on $check_path"
|
||||||
|
|
||||||
|
echo "running bandit"
|
||||||
|
bandit --recursive --skip B105,B108,B404,B603,B607 "$check_path"
|
||||||
|
echo "running black"
|
||||||
|
black --diff --color --check -l 79 "$check_path"
|
||||||
|
echo "running codespell"
|
||||||
|
codespell --skip="./.git" "$check_path"
|
||||||
|
echo "running flake8"
|
||||||
|
flake8 "$check_path" --count --max-complexity=12 --max-line-length=79 \
|
||||||
|
--show-source --statistics
|
||||||
|
echo "running isort"
|
||||||
|
isort --check-only --diff --profile black -l 79 "$check_path"
|
||||||
|
printf " \n> all validations passed\n"
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function sync_docker {
|
||||||
|
|
||||||
|
# check things
|
||||||
|
if [[ $(git branch --show-current) != 'master' ]]; then
|
||||||
|
echo 'you are not on master, dummy!'
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $(systemctl is-active docker) != 'active' ]]; then
|
||||||
|
echo "starting docker"
|
||||||
|
sudo systemctl start docker
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "latest tags:"
|
||||||
|
git tag
|
||||||
|
|
||||||
|
echo "latest docker images:"
|
||||||
|
sudo docker image ls bbilly1/tubearchivist
|
||||||
|
|
||||||
|
printf "\ncreate new version:\n"
|
||||||
|
read -r VERSION
|
||||||
|
|
||||||
|
echo "build and push $VERSION?"
|
||||||
|
read -rn 1
|
||||||
|
|
||||||
|
# start build
|
||||||
|
sudo docker buildx build \
|
||||||
|
--platform linux/amd64,linux/arm64 \
|
||||||
|
-t bbilly1/tubearchivist:latest \
|
||||||
|
-t bbilly1/tubearchivist:"$VERSION" --push .
|
||||||
|
|
||||||
|
# create release tag
|
||||||
|
echo "commits since last version:"
|
||||||
|
git log "$(git describe --tags --abbrev=0)"..HEAD --oneline
|
||||||
|
git tag -a "$VERSION" -m "new release version $VERSION"
|
||||||
|
git push all "$VERSION"
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# check package versions in requirements.txt for updates
|
||||||
|
python version_check.py
|
||||||
|
|
||||||
|
|
||||||
|
if [[ $1 == "blackhole" ]]; then
|
||||||
|
sync_blackhole
|
||||||
|
elif [[ $1 == "test" ]]; then
|
||||||
|
sync_test "$2"
|
||||||
|
elif [[ $1 == "validate" ]]; then
|
||||||
|
validate "$2"
|
||||||
|
elif [[ $1 == "docker" ]]; then
|
||||||
|
sync_docker
|
||||||
|
else
|
||||||
|
echo "valid options are: blackhole | test | validate | docker"
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
exit 0
|
@ -8,37 +8,36 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- 8000:8000
|
- 8000:8000
|
||||||
volumes:
|
volumes:
|
||||||
- media:/youtube
|
- ./volumes/tubearchivist/media:/youtube
|
||||||
- cache:/cache
|
- ./volumes/tubearchivist/cache:/cache
|
||||||
environment:
|
environment:
|
||||||
- ES_URL=http://archivist-es:9200 # needs protocol e.g. http and port
|
- ES_URL=http://archivist-es:9200
|
||||||
- REDIS_HOST=archivist-redis # don't add protocol
|
- REDIS_HOST=archivist-redis
|
||||||
- HOST_UID=1000
|
- HOST_UID=1000
|
||||||
- HOST_GID=1000
|
- HOST_GID=1000
|
||||||
- TA_USERNAME=tubearchivist # your initial TA credentials
|
- TA_USERNAME=tubearchivist
|
||||||
- TA_PASSWORD=verysecret # your initial TA credentials
|
- TA_PASSWORD=verysecret
|
||||||
- ELASTIC_PASSWORD=verysecret # set password for Elasticsearch
|
- ELASTIC_PASSWORD=verysecret
|
||||||
- TZ=America/New_York # set your time zone
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- archivist-es
|
- archivist-es
|
||||||
- archivist-redis
|
- archivist-redis
|
||||||
archivist-redis:
|
archivist-redis:
|
||||||
image: redislabs/rejson:latest # for arm64 use bbilly1/rejson
|
image: redislabs/rejson:latest
|
||||||
container_name: archivist-redis
|
container_name: archivist-redis
|
||||||
restart: always
|
restart: always
|
||||||
expose:
|
expose:
|
||||||
- "6379"
|
- "6379"
|
||||||
volumes:
|
volumes:
|
||||||
- redis:/data
|
- ./volumes/tubearchivist/redis:/data
|
||||||
depends_on:
|
depends_on:
|
||||||
- archivist-es
|
- archivist-es
|
||||||
archivist-es:
|
archivist-es:
|
||||||
image: bbilly1/tubearchivist-es # only for amd64, or use official es 7.17.2
|
image: docker.elastic.co/elasticsearch/elasticsearch:7.15.1
|
||||||
container_name: archivist-es
|
container_name: archivist-es
|
||||||
restart: always
|
restart: always
|
||||||
environment:
|
environment:
|
||||||
- "xpack.security.enabled=true"
|
- "xpack.security.enabled=true"
|
||||||
- "ELASTIC_PASSWORD=verysecret" # matching Elasticsearch password
|
- "ELASTIC_PASSWORD=verysecret"
|
||||||
- "discovery.type=single-node"
|
- "discovery.type=single-node"
|
||||||
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
|
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
|
||||||
ulimits:
|
ulimits:
|
||||||
@ -46,12 +45,6 @@ services:
|
|||||||
soft: -1
|
soft: -1
|
||||||
hard: -1
|
hard: -1
|
||||||
volumes:
|
volumes:
|
||||||
- es:/usr/share/elasticsearch/data # check for permission error when using bind mount, see readme
|
- ./volumes/tubearchivist/es:/usr/share/elasticsearch/data
|
||||||
expose:
|
expose:
|
||||||
- "9200"
|
- "9200"
|
||||||
|
|
||||||
volumes:
|
|
||||||
media:
|
|
||||||
cache:
|
|
||||||
redis:
|
|
||||||
es:
|
|
||||||
|
24
docs/Channels.md
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# Channels Overview and Channel Detail Page
|
||||||
|
|
||||||
|
The channels are organized on two different levels:
|
||||||
|
|
||||||
|
## Channels Overview
|
||||||
|
Accessible at `/channel/` of your Tube Archivist, the **Overview Page** shows a list of all channels you have indexed.
|
||||||
|
- You can filter that list to show or hide subscribed channels from the drop down menu. Clicking on the channel banner or the channel name will direct you to the *Channel Detail Page*.
|
||||||
|
- If you are subscribed to a channel a *Unsubscribe* button will show.
|
||||||
|
|
||||||
|
The **Subscribe to Channels** button <img src="assets/icon-add.png?raw=true" alt="add icon" width="20px" style="margin:0 5px;"> opens a text field to subscribe to a channel. You have a few options:
|
||||||
|
- Enter the YouTube channel ID, a 25 character alphanumeric string. For example *UCBa659QWEk1AI4Tg--mrJ2A*
|
||||||
|
- Enter the URL to the channel page on YouTube. For example *https://www.youtube.com/channel/UCBa659QWEk1AI4Tg--mrJ2A*
|
||||||
|
- Enter the channel name for example: *https://www.youtube.com/c/TomScottGo*.
|
||||||
|
- Enter the video URL for any video and let Tube Archivist extract the channel ID for you. For example *https://www.youtube.com/watch?v=2tdiKTSdE9Y*
|
||||||
|
- Add one per line.
|
||||||
|
|
||||||
|
The search icon <img src="assets/icon-search.png?raw=true" alt="search icon" width="20px" style="margin:0 5px;"> opens a text box to search for indexed channel names. Possible matches will show as you type.
|
||||||
|
|
||||||
|
## Channel Detail
|
||||||
|
Each channel will get a dedicated channel detail page accessible at `/channel/<channel-id>/` of your Tube Archivist. This page shows all the videos you have downloaded from this channel plus additional metadata.
|
||||||
|
- If you are subscribed to the channel, an *Unsubscribe* button will show.
|
||||||
|
- You can *Show* the channel description, that matches with the *About* tab on YouTube.
|
||||||
|
- The **Mark as Watched** button will mark all videos of this channel as watched.
|
||||||
|
- The button **Delete Channel** will delete the channel plus all videos of this channel, both media files and metadata.
|
35
docs/Downloads.md
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
# Downloads Page
|
||||||
|
Accessible at `/downloads/` of your Tube Archivist, this page handles all the download functionality.
|
||||||
|
|
||||||
|
|
||||||
|
## Rescan Subscriptions
|
||||||
|
The **Rescan Subscriptions** icon <img src="assets/icon-rescan.png?raw=true" alt="rescan icon" width="20px" style="margin:0 5px;"> will start a background task to look for new videos from the channels you are subscribed to. You can define the channel page size on the [settings page](Settings#subscriptions). With the default channel page size, expect this process to take around 2-3 seconds for each channel you are subscribed to. A status message will show the progress.
|
||||||
|
|
||||||
|
Then for every video found, **Tube Archivist** will skip the video if it has already been downloaded or if you added it to the *ignored* list before. All the other videos will get added to the download queue. Expect this to take around 1 second for each video as **Tube Archivist** needs to grab some additional metadata. New videos will get added at the bottom of the download queue.
|
||||||
|
|
||||||
|
## Download Queue
|
||||||
|
The **Start Download** icon <img src="assets/icon-download.png?raw=true" alt="download icon" width="20px" style="margin:0 5px;"> will start the download process starting from the top of the queue. Take a look at the relevant settings on the [Settings Page](Settings#downloads). Once the process started, a progress message will show with additional details and controls:
|
||||||
|
- The stop icon <img src="assets/icon-stop.png?raw=true" alt="stop icon" width="20px" style="margin:0 5px;"> will gracefully stop the download process, once the current video has been finished successfully.
|
||||||
|
- The cancel icon <img src="assets/icon-close-red.png?raw=true" alt="close icon" width="20px" style="margin:0 5px;"> is equivalent to killing the process and will stop the download immediately. Any leftover files will get deleted, the canceled video will still be available in the download queue.
|
||||||
|
|
||||||
|
## Add to Download Queue
|
||||||
|
The **Add to Download Queue** icon <img src="assets/icon-add.png?raw=true" alt="add icon" width="20px" style="margin:0 5px;"> opens a text field to manually add videos to the download queue. You have a few options:
|
||||||
|
- Add a link to a YouTube video. For example *https://www.youtube.com/watch?v=2tdiKTSdE9Y*.
|
||||||
|
- Add a YouTube video ID. For example *2tdiKTSdE9Y*.
|
||||||
|
- Add a link to a YouTube video by providing the shortened URL, for example *https://youtu.be/2tdiKTSdE9Y*.
|
||||||
|
- Add a Channel ID or Channel URL to add every available video to the download queue. This will ignore the channel page size as described before and is meant for an initial download of the whole channel. You can still ignore selected videos before starting the download.
|
||||||
|
- Add a channel name like for example *https://www.youtube.com/c/TomScottGo*.
|
||||||
|
- Add a playlist ID or URL to add every available video in the list to the download queue, for example *https://www.youtube.com/playlist?list=PL96C35uN7xGLLeET0dOWaKHkAlPsrkcha* or *PL96C35uN7xGLLeET0dOWaKHkAlPsrkcha*. Note that when you add a link to a video in a playlist, Tube Archivist assumes you want to download only the specific video and not the whole playlist, for example *https://www.youtube.com/watch?v=CINVwWHlzTY&list=PL96C35uN7xGLLeET0dOWaKHkAlPsrkcha* will only add one video *CINVwWHlzTY* to the queue.
|
||||||
|
- Add one link per line.
|
||||||
|
|
||||||
|
## The Download Queue
|
||||||
|
Below the three buttons you find the download queue. New items will get added at the bottom of the queue, the next video to download once you click on **Start Download** will be the first in the list.
|
||||||
|
|
||||||
|
Every video in the download queue has two buttons:
|
||||||
|
- **Ignore**: This will remove that video from the download queue and this video will not get added again, even when you **Rescan Subscriptions**.
|
||||||
|
- **Download now**: This will give priority to this video. If the download process is already running, the prioritized video will get downloaded as soon as the current video is finished. If there is no download process running, this will start downloading this single video and stop after that.
|
||||||
|
|
||||||
|
You can flip the view by activating **Show Only Ignored Videos**. This will show all videos you have previously *ignored*.
|
||||||
|
Every video in the ignored list has two buttons:
|
||||||
|
- **Forget**: This will delete the item form the ignored list.
|
||||||
|
- **Add to Queue**: This will add the ignored video back to the download queue.
|
27
docs/Home.md
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
# Tube Archivist Wiki
|
||||||
|
Welcome to the official Tube Archivist Wiki. This is an up-to-date documentation of user functionality.
|
||||||
|
|
||||||
|
Table of contents:
|
||||||
|
* [Main](Main): Tube Archivist landing page
|
||||||
|
* [Channels](Channels): Browse your channels, handle subscriptions
|
||||||
|
* [Downloads](Downloads): Scanning subscriptions, handle download queue
|
||||||
|
* [Settings](Settings): All the configuration options
|
||||||
|
* [Users](Users): User management admin interface
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
1. [Subscribe](Channels#channels-overview) to some of your favourite YouTube channels.
|
||||||
|
2. [Scan](Downloads#rescan-subscriptions) subscriptions to add the latest videos to the download queue.
|
||||||
|
3. [Add](Downloads#add-to-download-queue) additional videos, channels or playlist - ignore the ones you don't want to download.
|
||||||
|
4. [Download](Downloads#download-queue) and let **Tube Archivist** do it's thing.
|
||||||
|
5. Sit back and enjoy your archived and indexed collection!
|
||||||
|
|
||||||
|
## General Navigation
|
||||||
|
* Clicking on the channel name or the channel icon brings you to the dedicated channel page to show videos from that channel.
|
||||||
|
* Clicking on a video title brings you to the dedicated video page and shows additional details.
|
||||||
|
* Clicking on a video thumbnail opens the video player and starts streaming the selected video.
|
||||||
|
* Hover over the playing video to show additional control options.
|
||||||
|
|
||||||
|
|
||||||
|
An empty checkbox icon <img src="assets/icon-unseen.png?raw=true" alt="unseen icon" width="20px" style="margin:0 5px;"> will show for videos you haven't marked as watched. Click on it and the icon will change to a filled checkbox <img src="assets/icon-seen.png?raw=true" alt="seen icon" width="20px" style="margin:0 5px;"> indicating it as watched - click again to revert.
|
||||||
|
|
||||||
|
When available the <img src="assets/icon-gridview.png?raw=true" alt="gridview icon" width="20px" style="margin:0 5px;"> gridview icon will display the list in a grid, the <img src="assets/icon-listview.png?raw=true" alt="listview icon" width="20px" style="margin:0 5px;"> listview icon will arrange the items in a list.
|
10
docs/Main.md
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
# Tube Archivist Home Page Functionality
|
||||||
|
|
||||||
|
This is the landing page, when you first open **Tube Archivist**. You have a few options to sort and filter that view:
|
||||||
|
- With the **Sort Order** you can select how the "Recent Videos" are sorted:
|
||||||
|
- **Date Published**: Sorts the list by date when the video was published on YouTube, newest on top.
|
||||||
|
- **Date Downloaded**: Sorts the list based on when you have downloaded the video to your archive, newest on top.
|
||||||
|
- With **Hide Watched** you can filter out videos you have already marked as watched to only show unwatched videos.
|
||||||
|
- You can use those two options together to for example filter the list to *Hide Watched* videos **and** sort by date downloaded.
|
||||||
|
|
||||||
|
Additionally the search icon <img src="assets/icon-search.png?raw=true" alt="search icon" width="20px" style="margin:0 5px;"> opens a text field to search your collection.
|
73
docs/Settings.md
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
# Settings Page
|
||||||
|
Accessible at `/settings/` of your **Tube Archivist**, this page holds all the configurations and additional functionality related to the database.
|
||||||
|
|
||||||
|
Click on **Update Settings** at the bottom of the form to apply your configurations.
|
||||||
|
|
||||||
|
## Color scheme
|
||||||
|
Switch between the easy on the eyes dark theme and the burning bright theme.
|
||||||
|
|
||||||
|
## Archive View
|
||||||
|
- **Page Size**: Defines how many results get displayed on a given page. Same value goes for all archive views.
|
||||||
|
|
||||||
|
## Subscriptions
|
||||||
|
Settings related to the channel management.
|
||||||
|
- **Channel Page Size**: Defines how many pages will get analyzed by **Tube Archivist** each time you click on *Rescan Subscriptions*. The default page size used by yt-dlp is **50**, that's also the recommended value to set here. Any value higher will slow down the rescan process, for example if you set the value to 51, that means yt-dlp will have to go through 2 pages of results instead of 1 and by that doubling the time that process takes.
|
||||||
|
|
||||||
|
## Downloads
|
||||||
|
Settings related to the download process.
|
||||||
|
- **Download Limit**: Stop the download process after downloading the set quantity of videos.
|
||||||
|
- **Download Speed Limit**: Set your download speed limit in KB/s. This will pass the option `--limit-rate` to yt-dlp.
|
||||||
|
- **Throttled Rate Limit**: Restart download if the download speed drops below this value in KB/s. This will pass the option `--throttled-rate` to yt-dlp. Using this option might have a negative effect if you have an unstable or slow internet connection.
|
||||||
|
- **Sleep Interval**: Time in seconds to sleep between requests to YouTube. It's a good idea to set this to **3** seconds. Might be necessary to avoid throttling.
|
||||||
|
|
||||||
|
## Download Format
|
||||||
|
Additional settings passed to yt-dlp.
|
||||||
|
- **Format**: This controls which streams get downloaded and is equivalent to passing `--format` to yt-dlp. Use one of the recommended one or look at the documentation of [yt-dlp](https://github.com/yt-dlp/yt-dlp#format-selection). Please note: The option `--merge-output-format mp4` is automatically passed to yt-dlp to guarantee browser compatibility.
|
||||||
|
- **Embed Metadata**: This saves the available tags directly into the media file by passing `--embed-metadata` to yt-dlp.
|
||||||
|
- **Embed Thumbnail**: This will save the thumbnail into the media file by passing `--embed-thumbnail` to yt-dlp.
|
||||||
|
|
||||||
|
|
||||||
|
# Actions
|
||||||
|
Additional database functionality.
|
||||||
|
|
||||||
|
## Manual Media Files Import
|
||||||
|
So far this depends on the video you are trying to import to be still available on YouTube to get the metadata. Add the files you like to import to the */cache/import* folder. Then start the process from the settings page *Manual Media Files Import*. Make sure to follow one of the two methods below.
|
||||||
|
|
||||||
|
### Method 1:
|
||||||
|
Add a matching *.json* file with the media file. Both files need to have the same base name, for example:
|
||||||
|
- For the media file: \<base-name>.mp4
|
||||||
|
- For the JSON file: \<base-name>.info.json
|
||||||
|
- Alternate JSON file: \<base-name>.json
|
||||||
|
|
||||||
|
**Tube Archivist** then looks for the 'id' key within the JSON file to identify the video.
|
||||||
|
|
||||||
|
### Method 2:
|
||||||
|
Detect the YouTube ID from filename, this accepts the default yt-dlp naming convention for file names like:
|
||||||
|
- \<base-name>[\<youtube-id>].mp4
|
||||||
|
- The YouTube ID in square brackets at the end of the filename is the crucial part.
|
||||||
|
|
||||||
|
### Some notes:
|
||||||
|
- This will **consume** the files you put into the import folder: Files will get converted to mp4 if needed (this might take a long time...) and moved to the archive, *.json* files will get deleted upon completion to avoid having duplicates on the next run.
|
||||||
|
- Maybe start with a subset of your files to import to make sure everything goes well...
|
||||||
|
- Follow the logs to monitor progress and errors: `docker-compose logs -f tubearchivist`.
|
||||||
|
|
||||||
|
## Embed thumbnails into media file
|
||||||
|
This will write or overwrite all thumbnails in the media file using the downloaded thumbnail. This is only necessary if you didn't download the files with the option *Embed Thumbnail* enabled or want to make sure all media files get the newest thumbnail. Follow the docker-compose logs to monitor progress.
|
||||||
|
|
||||||
|
## Backup Database
|
||||||
|
This will backup your metadata into a zip file. The file will get stored at *cache/backup* and will contain the necessary files to restore the Elasticsearch index formatted **nd-json** files plus a complete export of the index in a set of conventional **json** files.
|
||||||
|
|
||||||
|
BE AWARE: This will **not** backup any media files, just the metadata from the Elasticsearch.
|
||||||
|
|
||||||
|
## Restore From Backup
|
||||||
|
The restore functionality will expect the same zip file in *cache/backup* as created from the **Backup database** function. This will recreate the index from the snapshot. If there are multiple backup files in the folder, the newest one will take priority.
|
||||||
|
|
||||||
|
BE AWARE: This will **replace** your current index with the one from the backup file. This won't restore any media files.
|
||||||
|
|
||||||
|
## Rescan Filesystem
|
||||||
|
This function will go through all your media files and looks at the whole index to try to find any issues:
|
||||||
|
- Should the filename not match with the indexed media url, this will rename the video files correctly and update the index with the new link.
|
||||||
|
- When you delete media files from the filesystem outside of the Tube Archivist interface, this will delete leftover metadata from the index.
|
||||||
|
- When you have media files that are not indexed yet, this will grab the metadata from YouTube like it was a newly downloaded video. This can be useful when restoring from an older backup file with missing metadata but already downloaded mediafiles. NOTE: This only works if the media files are named in the same convention as Tube Archivist does, particularly the YouTube ID needs to be at the same index in the filename, alternatively see above for *Manual Media Files Import*.
|
||||||
|
|
||||||
|
BE AWARE: There is no undo.
|
20
docs/Users.md
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
# User Management
|
||||||
|
|
||||||
|
For now, **Tube Archivist** is a single user application. You can create multiple users with different names and passwords, they will share the same videos and permissions but some interface configurations are on a per user basis. *More is on the roadmap*.
|
||||||
|
|
||||||
|
## Superuser
|
||||||
|
The first user gets created with the environment variables **TA_USERNAME** and **TA_PASSWORD** from your docker-compose file. That first user will automatically have *superuser* privileges.
|
||||||
|
|
||||||
|
## Admin Interface
|
||||||
|
When logged in from your *superuser* account, you are able to access the admin interface from the settings page or at `/admin/`. This interface holds all functionality for user management.
|
||||||
|
|
||||||
|
## Create additional users
|
||||||
|
From the admin interface when you click on *Accounts* you will get a list of all users. From there you can create additional users by clicking on *Add Account*, provide a name and confirm password and click on *Save* to create the user.
|
||||||
|
|
||||||
|
## Changing users
|
||||||
|
You can delete or change permissions and password of a user by clicking on the username from the *Accounts* list page and follow the interface from there. Changing the password of the *superuser* here will overwrite the password originally set with the environment variables.
|
||||||
|
|
||||||
|
## Reset
|
||||||
|
Delete all user configurations by deleting the file `cache/db.sqlite3` and restart the container. This will create the superuser again from the environment variables.
|
||||||
|
|
||||||
|
NOTE: Future improvements here will most likely require such a reset.
|
BIN
docs/assets/icon-add.png
Normal file
After Width: | Height: | Size: 2.5 KiB |
BIN
docs/assets/icon-close-blue.png
Normal file
After Width: | Height: | Size: 4.3 KiB |
BIN
docs/assets/icon-close-red.png
Normal file
After Width: | Height: | Size: 4.3 KiB |
BIN
docs/assets/icon-download.png
Normal file
After Width: | Height: | Size: 2.7 KiB |
BIN
docs/assets/icon-gridview.png
Normal file
After Width: | Height: | Size: 3.1 KiB |
BIN
docs/assets/icon-listview.png
Normal file
After Width: | Height: | Size: 2.8 KiB |
BIN
docs/assets/icon-rescan.png
Normal file
After Width: | Height: | Size: 4.4 KiB |
BIN
docs/assets/icon-search.png
Normal file
After Width: | Height: | Size: 5.1 KiB |
BIN
docs/assets/icon-seen.png
Normal file
After Width: | Height: | Size: 3.3 KiB |
BIN
docs/assets/icon-stop.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
docs/assets/icon-unseen.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
5
next-env.d.ts
vendored
@ -1,5 +0,0 @@
|
|||||||
/// <reference types="next" />
|
|
||||||
/// <reference types="next/image-types/global" />
|
|
||||||
|
|
||||||
// NOTE: This file should not be edited
|
|
||||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
|
@ -1,9 +0,0 @@
|
|||||||
/** @type {import('next').NextConfig} */
|
|
||||||
const nextConfig = {
|
|
||||||
images: {
|
|
||||||
domains: ["localhost", "tube.stiforr.tech"],
|
|
||||||
},
|
|
||||||
reactStrictMode: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = nextConfig;
|
|
18
nginx.conf
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
server {
|
||||||
|
|
||||||
|
listen 8000;
|
||||||
|
|
||||||
|
location /cache/ {
|
||||||
|
alias /cache/;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /media/ {
|
||||||
|
alias /youtube/;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
include uwsgi_params;
|
||||||
|
uwsgi_pass localhost:8080;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
36
package.json
@ -1,36 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "tubearchivist-frontend",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"private": true,
|
|
||||||
"scripts": {
|
|
||||||
"dev": "next dev",
|
|
||||||
"build": "next build",
|
|
||||||
"start": "next start",
|
|
||||||
"lint": "next lint",
|
|
||||||
"prepare": "husky install"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"next": "12.1.1",
|
|
||||||
"next-auth": "4.7.0",
|
|
||||||
"react": "18.0.0",
|
|
||||||
"react-dom": "18.0.0",
|
|
||||||
"react-player": "2.10.0",
|
|
||||||
"react-query": "3.39.1",
|
|
||||||
"sharp": "0.30.3"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/node": "17.0.23",
|
|
||||||
"@types/react": "17.0.43",
|
|
||||||
"@types/react-dom": "17.0.14",
|
|
||||||
"eslint": "8.12.0",
|
|
||||||
"eslint-config-next": "12.1.1",
|
|
||||||
"husky": "7.0.4",
|
|
||||||
"lint-staged": "12.4.0",
|
|
||||||
"prettier": "2.6.1",
|
|
||||||
"typescript": "4.5.5"
|
|
||||||
},
|
|
||||||
"lint-staged": {
|
|
||||||
"*.{ts,tsx}": "eslint --cache --fix",
|
|
||||||
"*.{ts,tsx,css,md}": "prettier --write"
|
|
||||||
}
|
|
||||||
}
|
|
Before Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 8.4 KiB |
Before Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 920 B |
Before Width: | Height: | Size: 747 B |
Before Width: | Height: | Size: 1012 B |
Before Width: | Height: | Size: 830 B |
Before Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 891 B |
Before Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 959 B |
Before Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 2.2 KiB |
@ -1,9 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<browserconfig>
|
|
||||||
<msapplication>
|
|
||||||
<tile>
|
|
||||||
<square150x150logo src="/static/favicon/mstile-150x150.png"/>
|
|
||||||
<TileColor>#01202e</TileColor>
|
|
||||||
</tile>
|
|
||||||
</msapplication>
|
|
||||||
</browserconfig>
|
|
Before Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 2.0 KiB |
@ -1,100 +0,0 @@
|
|||||||
<?xml version="1.0" standalone="no"?>
|
|
||||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
|
||||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
|
||||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="1000.000000pt" height="1000.000000pt" viewBox="0 0 1000.000000 1000.000000"
|
|
||||||
preserveAspectRatio="xMidYMid meet">
|
|
||||||
<metadata>
|
|
||||||
Created by potrace 1.14, written by Peter Selinger 2001-2017
|
|
||||||
</metadata>
|
|
||||||
<g transform="translate(0.000000,1000.000000) scale(0.100000,-0.100000)"
|
|
||||||
fill="#000000" stroke="none">
|
|
||||||
<path d="M4521 9370 c-4 -3 -7 -31 -6 -63 1 -32 -3 -61 -9 -66 -6 -5 -36 -12
|
|
||||||
-66 -16 -55 -7 -68 -9 -125 -19 -16 -3 -48 -8 -70 -12 -161 -24 -504 -118
|
|
||||||
-709 -193 -208 -77 -507 -215 -636 -294 -19 -12 -47 -29 -62 -37 -14 -8 -41
|
|
||||||
-24 -60 -34 -84 -48 -288 -189 -423 -293 -309 -239 -654 -606 -886 -943 -53
|
|
||||||
-78 -169 -258 -169 -264 0 -3 -21 -40 -47 -83 -97 -164 -313 -646 -294 -658
|
|
||||||
14 -8 670 -195 686 -195 8 0 15 9 15 19 0 11 5 23 10 26 6 3 10 13 10 21 0 8
|
|
||||||
14 44 30 79 17 35 30 67 30 70 0 3 21 48 47 100 l47 95 148 0 c81 0 276 1 433
|
|
||||||
1 157 0 310 -1 340 -1 l55 0 0 -615 0 -615 415 1 415 0 1 142 c0 78 0 355 1
|
|
||||||
615 l1 472 46 0 c25 0 322 0 659 0 l612 1 0 32 c1 44 -1 538 -2 660 0 63 -5
|
|
||||||
97 -12 98 -6 1 -581 1 -1278 0 -698 0 -1268 2 -1268 5 0 7 143 147 241 235 94
|
|
||||||
85 232 193 329 260 36 24 67 47 68 52 2 4 7 7 12 7 4 0 52 28 106 62 126 79
|
|
||||||
371 202 510 257 60 23 118 46 129 51 131 56 685 193 713 176 4 -2 7 -34 6 -70
|
|
||||||
l0 -66 162 0 162 0 0 503 0 502 -155 0 c-85 0 -158 -2 -162 -5z"/>
|
|
||||||
<path d="M5210 8905 l0 -365 48 -1 c26 0 54 -2 62 -4 8 -1 45 -5 82 -9 72 -6
|
|
||||||
198 -23 232 -31 12 -3 32 -7 46 -9 284 -48 643 -169 935 -314 527 -262 984
|
|
||||||
-651 1324 -1127 39 -55 75 -105 81 -112 5 -7 10 -17 10 -23 0 -5 4 -10 9 -10
|
|
||||||
5 0 14 -12 20 -27 6 -16 24 -48 40 -73 31 -47 61 -101 117 -208 30 -57 32 -65
|
|
||||||
16 -69 -75 -20 -117 -40 -113 -53 9 -35 83 -285 85 -287 1 -2 18 2 37 8 19 5
|
|
||||||
41 12 49 14 49 12 856 241 876 249 11 4 10 14 -2 57 -9 29 -17 60 -19 68 -7
|
|
||||||
32 -53 183 -57 186 -2 2 -22 -2 -43 -9 -93 -31 -88 -33 -126 46 -232 488 -557
|
|
||||||
933 -950 1303 -285 269 -647 520 -1014 705 -533 268 -1067 412 -1682 455 l-63
|
|
||||||
4 0 -364z"/>
|
|
||||||
<path d="M6122 7336 c-4 -6 -14 -31 -23 -56 -17 -47 -238 -630 -288 -760 -16
|
|
||||||
-41 -61 -160 -100 -265 -40 -104 -76 -199 -80 -210 -10 -23 -54 -138 -104
|
|
||||||
-275 -20 -52 -41 -108 -48 -125 -7 -16 -13 -32 -13 -35 -1 -3 -7 -18 -13 -35
|
|
||||||
-6 -16 -55 -145 -108 -285 -53 -140 -102 -271 -110 -290 -8 -19 -48 -125 -89
|
|
||||||
-235 -41 -110 -90 -240 -109 -290 -19 -49 -76 -200 -127 -335 -133 -356 -151
|
|
||||||
-402 -166 -437 -8 -17 -14 -34 -14 -37 0 -3 -15 -45 -34 -94 -19 -48 -61 -160
|
|
||||||
-95 -248 -33 -89 -65 -174 -72 -190 -22 -55 -28 -70 -98 -258 -39 -103 -71
|
|
||||||
-192 -71 -197 0 -5 182 -8 441 -7 l441 3 114 320 c63 176 126 352 140 390 14
|
|
||||||
39 46 129 70 200 25 72 50 139 55 150 5 11 22 58 38 105 29 84 89 254 171 480
|
|
||||||
23 63 47 133 54 155 12 36 70 199 131 365 13 36 60 169 105 295 44 127 85 239
|
|
||||||
90 250 4 11 62 173 129 360 197 557 186 530 195 504 3 -8 18 -52 35 -99 16
|
|
||||||
-47 54 -155 84 -240 30 -85 96 -272 146 -415 51 -143 96 -269 100 -280 5 -11
|
|
||||||
27 -72 49 -135 120 -343 143 -407 152 -422 6 -10 10 -24 10 -33 0 -8 6 -29 14
|
|
||||||
-47 8 -18 44 -116 80 -218 182 -521 327 -927 336 -942 6 -10 10 -23 10 -29 0
|
|
||||||
-9 134 -393 176 -502 8 -21 28 -78 45 -128 l31 -90 -49 -56 c-26 -31 -78 -88
|
|
||||||
-115 -127 -64 -68 -68 -70 -81 -51 -8 11 -28 37 -44 57 l-30 37 -124 -91 c-68
|
|
||||||
-50 -123 -96 -123 -104 1 -9 281 -404 327 -459 4 -5 62 -84 128 -175 65 -91
|
|
||||||
124 -166 129 -168 8 -3 73 39 90 58 3 3 42 32 88 64 l82 58 -40 56 -39 56 94
|
|
||||||
96 c558 566 945 1258 1130 2025 18 77 38 167 44 200 6 33 13 69 15 80 6 33 19
|
|
||||||
119 21 140 2 11 6 43 9 70 35 253 40 715 11 930 -2 17 -9 75 -15 129 -7 55
|
|
||||||
-16 109 -20 122 -5 12 -7 24 -5 26 11 11 -85 428 -98 428 -11 0 -680 -190
|
|
||||||
-685 -195 -2 -1 8 -47 21 -101 14 -55 28 -115 31 -134 3 -19 8 -46 11 -60 10
|
|
||||||
-53 19 -109 29 -195 4 -33 9 -70 11 -82 8 -53 18 -259 18 -369 -1 -400 -74
|
|
||||||
-812 -213 -1194 -23 -63 -44 -123 -47 -132 -9 -29 -20 -21 -35 25 -22 62 -198
|
|
||||||
529 -208 552 -5 11 -69 178 -141 370 -72 193 -187 499 -256 680 -68 182 -164
|
|
||||||
436 -213 565 -48 129 -93 246 -98 260 -6 14 -27 70 -47 125 -19 55 -40 109
|
|
||||||
-45 120 -8 18 -191 503 -224 595 -15 42 -95 253 -139 366 l-31 80 -398 0
|
|
||||||
c-270 0 -401 -3 -406 -10z"/>
|
|
||||||
<path d="M720 6002 c-47 -160 -68 -237 -64 -243 2 -4 28 -13 56 -19 29 -7 56
|
|
||||||
-14 59 -16 4 -2 3 -23 -1 -46 -5 -24 -11 -61 -14 -83 -4 -23 -8 -50 -10 -60
|
|
||||||
-2 -11 -7 -47 -10 -80 -4 -33 -9 -73 -11 -90 -31 -231 -26 -701 11 -955 3 -25
|
|
||||||
8 -61 10 -80 2 -19 5 -44 8 -55 3 -11 8 -42 12 -70 3 -27 8 -53 10 -56 2 -4 6
|
|
||||||
-24 10 -45 20 -134 154 -594 209 -719 7 -16 27 -66 45 -110 59 -147 189 -400
|
|
||||||
297 -579 89 -147 270 -400 359 -502 11 -12 39 -46 64 -75 74 -89 358 -369 465
|
|
||||||
-459 55 -46 110 -92 121 -102 22 -19 22 -19 107 99 48 65 90 123 94 128 35 42
|
|
||||||
243 336 242 342 0 5 -38 40 -85 78 -313 258 -611 618 -818 988 -63 114 -176
|
|
||||||
341 -176 355 0 5 -9 26 -19 48 -54 113 -171 502 -200 669 -6 33 -13 69 -15 80
|
|
||||||
-8 43 -21 134 -31 210 -19 156 -24 420 -12 605 8 119 13 166 33 295 5 28 7 53
|
|
||||||
6 58 -2 8 24 3 99 -18 l45 -13 23 82 c12 44 29 106 38 136 30 104 34 95 -54
|
|
||||||
118 -43 11 -109 29 -148 40 -38 11 -104 30 -145 41 -41 12 -79 23 -85 25 -5 1
|
|
||||||
-28 8 -50 14 -22 7 -112 33 -200 58 -88 25 -181 52 -207 59 l-48 14 -20 -67z"/>
|
|
||||||
<path d="M2693 5210 c-48 -11 -119 -56 -148 -95 -112 -146 -57 -352 115 -428
|
|
||||||
43 -19 68 -20 560 -21 449 -1 521 1 563 15 65 22 114 64 151 129 27 48 31 64
|
|
||||||
31 130 0 116 -49 200 -149 253 -41 22 -44 22 -566 23 -289 1 -539 -2 -557 -6z"/>
|
|
||||||
<path d="M6425 5138 c-50 -19 -102 -55 -128 -90 -56 -73 -55 -54 -55 -901 -1
|
|
||||||
-430 -1 -793 -2 -807 0 -21 -26 1 -169 148 -93 94 -173 172 -178 172 -4 0 -58
|
|
||||||
-50 -118 -110 l-110 -110 25 -24 c14 -12 202 -204 419 -424 217 -221 397 -402
|
|
||||||
401 -402 3 0 199 192 435 426 l429 425 -114 114 -115 114 -177 -178 -178 -178
|
|
||||||
-1 381 c-2 1238 -2 1258 -24 1301 -26 52 -71 98 -126 126 -51 26 -166 35 -214
|
|
||||||
17z"/>
|
|
||||||
<path d="M2814 4501 c-2 -2 -4 -415 -4 -918 l0 -913 415 0 415 0 0 28 c4 223
|
|
||||||
0 1795 -4 1799 -6 7 -816 10 -822 4z"/>
|
|
||||||
<path d="M3181 2038 c-8 -13 -44 -63 -81 -113 -36 -49 -101 -137 -143 -195
|
|
||||||
-42 -58 -80 -109 -84 -115 -16 -20 -88 -119 -103 -141 -8 -13 -48 -68 -89
|
|
||||||
-124 -40 -55 -72 -103 -70 -105 2 -2 60 -44 129 -94 l125 -91 25 32 c62 79 35
|
|
||||||
81 231 -15 382 -187 717 -298 1149 -382 8 -1 36 -5 61 -9 25 -4 57 -9 70 -11
|
|
||||||
45 -7 183 -24 264 -32 105 -10 629 -10 725 0 116 13 131 14 195 22 58 7 70 9
|
|
||||||
125 19 14 3 42 8 63 11 48 7 174 32 252 51 33 8 67 16 76 19 145 33 419 126
|
|
||||||
599 202 122 52 413 195 418 206 2 4 8 7 12 7 11 0 193 108 273 162 53 36 57
|
|
||||||
41 45 59 -107 155 -406 558 -415 558 -7 1 -44 -19 -82 -44 -236 -153 -537
|
|
||||||
-295 -814 -386 -97 -31 -291 -85 -342 -94 -16 -2 -41 -7 -55 -10 -39 -9 -240
|
|
||||||
-43 -288 -49 -212 -28 -609 -29 -822 -2 -14 2 -52 7 -85 11 -33 4 -69 9 -80
|
|
||||||
12 -11 2 -33 6 -50 9 -211 34 -520 123 -735 211 -86 36 -291 130 -299 138 -2
|
|
||||||
3 15 30 38 60 24 31 41 58 39 60 -11 11 -243 178 -252 181 -6 2 -17 -6 -25
|
|
||||||
-18z"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 6.6 KiB |
@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "TubeArchivist",
|
|
||||||
"short_name": "TubeArchivist",
|
|
||||||
"icons": [
|
|
||||||
{
|
|
||||||
"src": "/static/favicon/android-chrome-192x192.png",
|
|
||||||
"sizes": "192x192",
|
|
||||||
"type": "image/png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "/static/favicon/android-chrome-512x512.png",
|
|
||||||
"sizes": "512x512",
|
|
||||||
"type": "image/png"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"theme_color": "#01202e",
|
|
||||||
"background_color": "#01202e",
|
|
||||||
"display": "standalone"
|
|
||||||
}
|
|
@ -1,75 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
|
||||||
|
|
||||||
<svg
|
|
||||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
|
||||||
xmlns:cc="http://creativecommons.org/ns#"
|
|
||||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
||||||
width="210mm"
|
|
||||||
height="210mm"
|
|
||||||
viewBox="0 0 210 210"
|
|
||||||
version="1.1"
|
|
||||||
id="svg1566"
|
|
||||||
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
|
|
||||||
sodipodi:docname="Icons_exit 05.svg">
|
|
||||||
<defs
|
|
||||||
id="defs1560" />
|
|
||||||
<sodipodi:namedview
|
|
||||||
id="base"
|
|
||||||
pagecolor="#ffffff"
|
|
||||||
bordercolor="#666666"
|
|
||||||
borderopacity="1.0"
|
|
||||||
inkscape:pageopacity="0.0"
|
|
||||||
inkscape:pageshadow="2"
|
|
||||||
inkscape:zoom="0.35355339"
|
|
||||||
inkscape:cx="963.7258"
|
|
||||||
inkscape:cy="291.01609"
|
|
||||||
inkscape:document-units="mm"
|
|
||||||
inkscape:current-layer="layer1"
|
|
||||||
showgrid="false"
|
|
||||||
inkscape:window-width="1920"
|
|
||||||
inkscape:window-height="1009"
|
|
||||||
inkscape:window-x="-8"
|
|
||||||
inkscape:window-y="-8"
|
|
||||||
inkscape:window-maximized="1" />
|
|
||||||
<metadata
|
|
||||||
id="metadata1563">
|
|
||||||
<rdf:RDF>
|
|
||||||
<cc:Work
|
|
||||||
rdf:about="">
|
|
||||||
<dc:format>image/svg+xml</dc:format>
|
|
||||||
<dc:type
|
|
||||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
|
||||||
<dc:title></dc:title>
|
|
||||||
</cc:Work>
|
|
||||||
</rdf:RDF>
|
|
||||||
</metadata>
|
|
||||||
<g
|
|
||||||
inkscape:label="Ebene 1"
|
|
||||||
inkscape:groupmode="layer"
|
|
||||||
id="layer1"
|
|
||||||
transform="translate(0,-87)">
|
|
||||||
<path
|
|
||||||
style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.35654187;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:markers fill stroke"
|
|
||||||
d="M 106.49932,87.901069 C 49.504302,87.900974 3.3006913,134.10459 3.3007713,191.0996 c 0,0.30098 0.003,0.60131 0.005,0.90167 v 0 c -0.003,0.29952 -0.006,0.59901 -0.006,0.89912 -8e-5,56.99502 46.2035307,103.19865 103.1985287,103.19854 23.01714,-0.0773 45.34783,-7.84709 63.44155,-22.07425 0,0 9.01874,-8.71006 2.40579,-16.41737 -6.61297,-7.70731 -19.11222,0.3185 -19.11222,0.3185 -13.60985,9.81394 -29.95596,15.11012 -46.73512,15.14236 -44.275428,0 -80.167758,-35.89234 -80.167758,-80.16778 0,-0.30097 0.003,-0.60148 0.006,-0.90166 h -5.2e-4 c -0.003,-0.29934 -0.006,-0.59901 -0.006,-0.89913 0,-44.27545 35.89234,-80.16777 80.167778,-80.16777 16.77916,0.0322 33.12527,5.32843 46.73512,15.14236 0,0 12.49925,8.02581 19.11222,0.3185 6.61295,-7.70732 -2.4058,-16.41739 -2.4058,-16.41739 C 151.84561,95.74815 129.51494,87.97828 106.4978,87.901069 Z m 54.30959,56.450221 -12.13663,11.69622 20.15864,20.93332 -93.932488,-1.4899 c -9.22763,-0.17349 -16.77655,6.07423 -16.92587,14.00904 l 0.002,0.002 c -0.0149,1.82673 -0.0235,3.40102 0,4.99598 l -0.002,0.002 c 0.14932,7.93483 7.69824,14.18254 16.92587,14.00905 l 93.932488,-1.48991 -20.15864,20.93333 12.13663,11.69622 34.0585,-35.35536 11.82982,-12.29208 h 0.003 l -9.9e-4,-0.002 9.9e-4,-9.9e-4 h -0.003 l -11.82982,-12.29208 z"
|
|
||||||
id="path1405"
|
|
||||||
inkscape:connector-curvature="0"
|
|
||||||
sodipodi:nodetypes="cccccccsccsccsccscccccccccccccccccccccc" />
|
|
||||||
<path
|
|
||||||
style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.39729571;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:markers fill stroke"
|
|
||||||
d="m 506.57967,92.503023 c -57.98068,-1e-4 -104.98336,47.002567 -104.98326,104.983257 1.9e-4,57.98049 47.00276,104.98284 104.98326,104.98273 23.42489,-0.0758 46.15146,-7.98387 57.83458,-18.08923 11.68313,-10.10537 12.15613,-18.62993 7.38675,-23.04107 v -0.002 c -4.7711,-4.41269 -12.38099,-1.9587 -17.69245,2.25103 -13.83538,9.99805 -30.45915,15.40285 -47.52888,15.4528 -45.04116,0 -81.55421,-36.51305 -81.5542,-81.55419 0,-45.04114 36.51307,-81.5542 81.5542,-81.5542 17.06933,0.0328 33.21884,5.19482 43.16812,12.86758 9.94929,7.67275 17.33418,9.17607 22.1053,4.76338 v -0.002 c 4.77116,-4.41278 5.55882,-12.9887 -0.73482,-18.60197 -18.40654,-14.47308 -41.1234,-22.377337 -64.5386,-22.455877 z m 55.24881,57.426467 -12.34652,11.8985 20.50728,21.29534 -95.55697,-1.51567 c -9.38721,-0.17649 -17.06669,6.17929 -17.21858,14.25133 l 0.003,0.002 c -0.15192,8.07203 7.28245,14.71295 16.66978,14.88953 l 95.22519,1.50947 -21.06332,20.28455 12.04579,12.49846 36.06808,-34.74464 0.005,0.005 12.34654,-11.89954 -12.03701,-12.50724 z m 35.17874,98.71801 0.69918,0.67386 c 0.13539,-0.22412 0.26991,-0.44874 0.4036,-0.67386 z"
|
|
||||||
id="path1405-6"
|
|
||||||
inkscape:connector-curvature="0"
|
|
||||||
sodipodi:nodetypes="ccccccccsczccccccccccccccccccccccc" />
|
|
||||||
<path
|
|
||||||
style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.39729571;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:markers fill stroke"
|
|
||||||
d="m 740.89945,94.730897 c -57.98068,-9.6e-5 -104.98334,47.002563 -104.98325,104.983253 1.9e-4,57.98049 47.00276,104.98284 104.98325,104.98274 23.42488,-0.0758 46.15145,-7.98387 64.5635,-22.46581 l -17.03461,-16.41553 c -13.83537,9.99805 -30.45916,15.40285 -47.52889,15.4528 -45.04113,0 -81.55419,-36.51306 -81.55419,-81.5542 0,-45.04114 36.51306,-81.55419 81.55419,-81.55419 17.06934,0.0328 33.69814,5.42058 47.54336,15.40423 l 16.99534,-16.3773 c -18.40663,-14.4732 -41.12349,-22.377447 -64.5387,-22.455993 z m 55.24882,57.426473 -12.34653,11.8985 20.50728,21.29534 -95.55696,-1.51567 c -9.38721,-0.17649 -17.06668,6.17928 -17.21858,14.25132 l 0.002,0.002 c -0.1519,8.07203 7.28245,14.71295 16.66978,14.88953 l 95.22519,1.50947 -21.06332,20.28455 12.04578,12.49846 36.06808,-34.74465 0.005,0.005 12.34653,-11.89953 -12.03699,-12.50725 z m 35.17873,98.718 0.69919,0.67386 c 0.13538,-0.22412 0.26991,-0.44874 0.40359,-0.67386 z"
|
|
||||||
id="path1405-9"
|
|
||||||
inkscape:connector-curvature="0"
|
|
||||||
sodipodi:nodetypes="ccccccsccccccccccccccccccccccc" />
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 6.0 KiB |
@ -1,63 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
|
||||||
|
|
||||||
<svg
|
|
||||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
|
||||||
xmlns:cc="http://creativecommons.org/ns#"
|
|
||||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
||||||
width="500"
|
|
||||||
height="500"
|
|
||||||
viewBox="0 0 132.29197 132.29167"
|
|
||||||
version="1.1"
|
|
||||||
id="svg1303"
|
|
||||||
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
|
|
||||||
sodipodi:docname="Icons_seen.svg">
|
|
||||||
<defs
|
|
||||||
id="defs1297" />
|
|
||||||
<sodipodi:namedview
|
|
||||||
id="base"
|
|
||||||
pagecolor="#ffffff"
|
|
||||||
bordercolor="#666666"
|
|
||||||
borderopacity="1.0"
|
|
||||||
inkscape:pageopacity="0.0"
|
|
||||||
inkscape:pageshadow="2"
|
|
||||||
inkscape:zoom="1.0105705"
|
|
||||||
inkscape:cx="84.208758"
|
|
||||||
inkscape:cy="136.94831"
|
|
||||||
inkscape:document-units="mm"
|
|
||||||
inkscape:current-layer="layer1"
|
|
||||||
showgrid="false"
|
|
||||||
units="px"
|
|
||||||
inkscape:window-width="1920"
|
|
||||||
inkscape:window-height="1017"
|
|
||||||
inkscape:window-x="-8"
|
|
||||||
inkscape:window-y="-8"
|
|
||||||
inkscape:window-maximized="1" />
|
|
||||||
<metadata
|
|
||||||
id="metadata1300">
|
|
||||||
<rdf:RDF>
|
|
||||||
<cc:Work
|
|
||||||
rdf:about="">
|
|
||||||
<dc:format>image/svg+xml</dc:format>
|
|
||||||
<dc:type
|
|
||||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
|
||||||
<dc:title />
|
|
||||||
</cc:Work>
|
|
||||||
</rdf:RDF>
|
|
||||||
</metadata>
|
|
||||||
<g
|
|
||||||
inkscape:label="Ebene 1"
|
|
||||||
inkscape:groupmode="layer"
|
|
||||||
id="layer1"
|
|
||||||
transform="translate(0,-164.70764)">
|
|
||||||
<path
|
|
||||||
style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0;stroke-linecap:round;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:markers fill stroke"
|
|
||||||
d="M 66.145984,191.3255 A 71.797122,73.404487 0 0 0 2.5025987,230.88314 71.797122,73.404487 0 0 0 66.145984,270.38145 71.797122,73.404487 0 0 0 129.78937,230.82387 71.797122,73.404487 0 0 0 66.145984,191.3255 Z m -4.921549,7.22394 a 32.724755,32.724755 0 0 0 -13.334395,5.17759 12.828107,12.828107 0 0 1 2.180958,-0.18652 12.828107,12.828107 0 0 1 12.828141,12.82816 12.828107,12.828107 0 0 1 -12.828141,12.82822 12.828107,12.828107 0 0 1 -12.828192,-12.82822 12.828107,12.828107 0 0 1 0.04509,-0.91548 32.724755,32.724755 0 0 0 -3.86581,15.39964 32.724755,32.724755 0 0 0 27.161922,32.24981 A 59.09757,60.420619 0 0 1 13.7604,230.8774 59.09757,60.420619 0 0 1 61.224664,198.54948 Z m 10.482345,0.0563 a 59.09757,60.420619 0 0 1 46.82502,32.22523 59.09757,60.420619 0 0 1 -47.393377,32.32497 32.724755,32.724755 0 0 0 27.73169,-32.30187 32.724755,32.724755 0 0 0 -27.163333,-32.24833 z"
|
|
||||||
id="path1091"
|
|
||||||
inkscape:connector-curvature="0" />
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 2.8 KiB |
@ -1,67 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
|
||||||
|
|
||||||
<svg
|
|
||||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
|
||||||
xmlns:cc="http://creativecommons.org/ns#"
|
|
||||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
||||||
width="500"
|
|
||||||
height="500"
|
|
||||||
viewBox="0 0 132.29197 132.29167"
|
|
||||||
version="1.1"
|
|
||||||
id="svg1303"
|
|
||||||
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
|
|
||||||
sodipodi:docname="Icons_sort.svg">
|
|
||||||
<defs
|
|
||||||
id="defs1297" />
|
|
||||||
<sodipodi:namedview
|
|
||||||
id="base"
|
|
||||||
pagecolor="#ffffff"
|
|
||||||
bordercolor="#666666"
|
|
||||||
borderopacity="1.0"
|
|
||||||
inkscape:pageopacity="0.0"
|
|
||||||
inkscape:pageshadow="2"
|
|
||||||
inkscape:zoom="0.6482696"
|
|
||||||
inkscape:cx="214.8721"
|
|
||||||
inkscape:cy="136.02434"
|
|
||||||
inkscape:document-units="mm"
|
|
||||||
inkscape:current-layer="g855"
|
|
||||||
showgrid="false"
|
|
||||||
units="px"
|
|
||||||
inkscape:window-width="957"
|
|
||||||
inkscape:window-height="893"
|
|
||||||
inkscape:window-x="941"
|
|
||||||
inkscape:window-y="13"
|
|
||||||
inkscape:window-maximized="0" />
|
|
||||||
<metadata
|
|
||||||
id="metadata1300">
|
|
||||||
<rdf:RDF>
|
|
||||||
<cc:Work
|
|
||||||
rdf:about="">
|
|
||||||
<dc:format>image/svg+xml</dc:format>
|
|
||||||
<dc:type
|
|
||||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
|
||||||
<dc:title></dc:title>
|
|
||||||
</cc:Work>
|
|
||||||
</rdf:RDF>
|
|
||||||
</metadata>
|
|
||||||
<g
|
|
||||||
inkscape:label="Ebene 1"
|
|
||||||
inkscape:groupmode="layer"
|
|
||||||
id="layer1"
|
|
||||||
transform="translate(0,-164.70764)">
|
|
||||||
<g
|
|
||||||
id="g855"
|
|
||||||
transform="matrix(1.9016362,0,0,1.9016362,-197.93838,-58.9418)">
|
|
||||||
<path
|
|
||||||
style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.55118108;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:markers fill stroke"
|
|
||||||
d="M 341.125 82.275391 L 315.50977 106.92773 L 241.84375 177.89258 L 266.21289 203.17969 L 309.82617 161.17969 L 306.72266 379.375 C 306.36114 398.60059 319.37807 414.32761 335.91016 414.63867 L 335.91406 414.63477 C 352.44586 414.94597 366.04647 399.71987 366.4082 380.49414 L 369.5 162.97852 L 411.04297 206.11719 L 436.64062 181.44727 L 365.48242 107.57812 L 365.49609 107.5625 L 341.125 82.275391 z M 175.93359 82.277344 L 175.92969 82.28125 C 159.39782 81.970041 145.79728 97.196144 145.43555 116.42188 L 142.34375 333.9375 L 100.80078 290.79883 L 75.203125 315.46875 L 146.36133 389.33789 L 146.3457 389.35156 L 170.7168 414.63867 L 196.33203 389.98633 L 270 319.02344 L 245.63086 293.73633 L 202.01758 335.73633 L 205.12109 117.54102 C 205.48261 98.315428 192.46568 82.588409 175.93359 82.277344 z "
|
|
||||||
transform="matrix(0.13913489,0,0,0.13913489,104.08846,117.60887)"
|
|
||||||
id="path814" />
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 2.9 KiB |
@ -1,76 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
|
||||||
|
|
||||||
<svg
|
|
||||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
|
||||||
xmlns:cc="http://creativecommons.org/ns#"
|
|
||||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
||||||
width="500"
|
|
||||||
height="500"
|
|
||||||
viewBox="0 0 132.29197 132.29167"
|
|
||||||
version="1.1"
|
|
||||||
id="svg1303"
|
|
||||||
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
|
|
||||||
sodipodi:docname="icon_thumb.svg">
|
|
||||||
<defs
|
|
||||||
id="defs1297" />
|
|
||||||
<sodipodi:namedview
|
|
||||||
id="base"
|
|
||||||
pagecolor="#ffffff"
|
|
||||||
bordercolor="#666666"
|
|
||||||
borderopacity="1.0"
|
|
||||||
inkscape:pageopacity="0.0"
|
|
||||||
inkscape:pageshadow="2"
|
|
||||||
inkscape:zoom="1.5046797"
|
|
||||||
inkscape:cx="35.718548"
|
|
||||||
inkscape:cy="203.39339"
|
|
||||||
inkscape:document-units="mm"
|
|
||||||
inkscape:current-layer="layer1"
|
|
||||||
showgrid="false"
|
|
||||||
units="px"
|
|
||||||
inkscape:window-width="1920"
|
|
||||||
inkscape:window-height="1009"
|
|
||||||
inkscape:window-x="-8"
|
|
||||||
inkscape:window-y="-8"
|
|
||||||
inkscape:window-maximized="1"
|
|
||||||
showguides="true"
|
|
||||||
inkscape:guide-bbox="true">
|
|
||||||
<sodipodi:guide
|
|
||||||
position="80.99058,65.965029"
|
|
||||||
orientation="0,1"
|
|
||||||
id="guide816"
|
|
||||||
inkscape:locked="false" />
|
|
||||||
<sodipodi:guide
|
|
||||||
position="65.965178,49.107299"
|
|
||||||
orientation="1,0"
|
|
||||||
id="guide818"
|
|
||||||
inkscape:locked="false" />
|
|
||||||
</sodipodi:namedview>
|
|
||||||
<metadata
|
|
||||||
id="metadata1300">
|
|
||||||
<rdf:RDF>
|
|
||||||
<cc:Work
|
|
||||||
rdf:about="">
|
|
||||||
<dc:format>image/svg+xml</dc:format>
|
|
||||||
<dc:type
|
|
||||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
|
||||||
<dc:title />
|
|
||||||
</cc:Work>
|
|
||||||
</rdf:RDF>
|
|
||||||
</metadata>
|
|
||||||
<g
|
|
||||||
inkscape:label="Ebene 1"
|
|
||||||
inkscape:groupmode="layer"
|
|
||||||
id="layer1"
|
|
||||||
transform="translate(0,-164.70764)">
|
|
||||||
<path
|
|
||||||
style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0;stroke-linecap:round;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:markers fill stroke"
|
|
||||||
d="m 72.35681,178.12175 c -0.545876,-0.0192 -1.121779,0.0192 -1.729242,0.12259 -12.959134,2.14891 4.226681,21.04642 -10.867262,31.83193 -12.617074,10.52505 -17.206737,7.22264 -17.332488,10.67172 v 33.88171 c 1.891308,5.00813 14.483922,10.12847 27.868234,11.21944 13.384309,1.09096 32.098988,-1.73801 32.305468,-7.56421 0.10989,-5.27572 -2.896223,-5.39662 -2.896223,-5.39662 0,0 6.589043,-0.40752 6.808883,-6.12278 0.10966,-5.60545 -6.38265,-6.40691 -6.38265,-6.40691 0,0 8.31589,-0.23607 8.31589,-6.01832 0,-5.91485 -6.39662,-6.38072 -8.37539,-6.48104 1.22776,-0.0747 6.07353,-1.75141 6.04505,-7.59582 -0.0287,-6.03384 -9.280896,-5.53597 -25.919173,-6.63279 1.919791,-11.19377 3.258461,-35.14851 -7.841097,-35.50894 z m -45.69253,38.36631 c -1.562226,0 -2.819733,1.04793 -2.819733,2.34979 v 38.25943 c 0,1.30191 1.257507,2.34983 2.819733,2.34983 h 10.526021 c 1.562231,0 2.819739,-1.04792 2.819739,-2.34983 v -38.25943 c 0,-1.30186 -1.257508,-2.34979 -2.819739,-2.34979 z"
|
|
||||||
id="rect1278"
|
|
||||||
inkscape:connector-curvature="0" />
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 3.2 KiB |
@ -1,148 +0,0 @@
|
|||||||
function initializeCastApi() {
|
|
||||||
cast.framework.CastContext.getInstance().setOptions({
|
|
||||||
receiverApplicationId: chrome.cast.media.DEFAULT_MEDIA_RECEIVER_APP_ID, // Use built in reciver app on cast device, see https://developers.google.com/cast/docs/styled_receiver if you want to be able to add a theme, splash screen or watermark. Has a $5 one time fee.
|
|
||||||
autoJoinPolicy: chrome.cast.AutoJoinPolicy.ORIGIN_SCOPED
|
|
||||||
});
|
|
||||||
|
|
||||||
var player = new cast.framework.RemotePlayer();
|
|
||||||
var playerController = new cast.framework.RemotePlayerController(player);
|
|
||||||
|
|
||||||
// Add event listerner to check if a connection to a cast device is initiated
|
|
||||||
playerController.addEventListener(
|
|
||||||
cast.framework.RemotePlayerEventType.IS_CONNECTED_CHANGED, function() {
|
|
||||||
castConnectionChange(player)
|
|
||||||
}
|
|
||||||
);
|
|
||||||
playerController.addEventListener(
|
|
||||||
cast.framework.RemotePlayerEventType.CURRENT_TIME_CHANGED, function() {
|
|
||||||
castVideoProgress(player)
|
|
||||||
}
|
|
||||||
);
|
|
||||||
playerController.addEventListener(
|
|
||||||
cast.framework.RemotePlayerEventType.IS_PAUSED_CHANGED, function() {
|
|
||||||
castVideoPaused(player)
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function castConnectionChange(player) {
|
|
||||||
// If cast connection is initialized start cast
|
|
||||||
if (player.isConnected) {
|
|
||||||
// console.log("Cast Connected.");
|
|
||||||
castStart();
|
|
||||||
} else if (!player.isConnected) {
|
|
||||||
// console.log("Cast Disconnected.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function castVideoProgress(player) {
|
|
||||||
var videoId = getVideoPlayerVideoId();
|
|
||||||
if (player.mediaInfo.contentId.includes(videoId)) {
|
|
||||||
var currentTime = player.currentTime;
|
|
||||||
var duration = player.duration;
|
|
||||||
if ((currentTime % 10) <= 1.0 && currentTime != 0 && duration != 0) { // Check progress every 10 seconds or else progress is checked a few times a second
|
|
||||||
postVideoProgress(videoId, currentTime);
|
|
||||||
setProgressBar(videoId, currentTime, duration);
|
|
||||||
if (!getVideoPlayerWatchStatus()) { // Check if video is already marked as watched
|
|
||||||
if (watchedThreshold(currentTime, duration)) {
|
|
||||||
isWatched(videoId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function castVideoPaused(player) {
|
|
||||||
var videoId = getVideoPlayerVideoId();
|
|
||||||
var currentTime = player.currentTime;
|
|
||||||
var duration = player.duration;
|
|
||||||
if (player.mediaInfo != null) {
|
|
||||||
if (player.mediaInfo.contentId.includes(videoId)) {
|
|
||||||
if (currentTime != 0 && duration != 0) {
|
|
||||||
postVideoProgress(videoId, currentTime);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function castStart() {
|
|
||||||
var castSession = cast.framework.CastContext.getInstance().getCurrentSession();
|
|
||||||
// Check if there is already media playing on the cast target to prevent recasting on page reload or switching to another video page
|
|
||||||
if (!castSession.getMediaSession()) {
|
|
||||||
var videoId = getVideoPlayerVideoId();
|
|
||||||
var videoData = getVideoData(videoId);
|
|
||||||
var contentId = getURL() + videoData.data.media_url;
|
|
||||||
var contentTitle = videoData.data.title;
|
|
||||||
var contentImage = getURL() + videoData.data.vid_thumb_url;
|
|
||||||
|
|
||||||
contentType = 'video/mp4'; // Set content type, only videos right now so it is hard coded
|
|
||||||
contentCurrentTime = getVideoPlayerCurrentTime(); // Get video's current position
|
|
||||||
contentActiveSubtitle = [];
|
|
||||||
// Check if a subtitle is turned on.
|
|
||||||
for (var i = 0; i < getVideoPlayer().textTracks.length; i++) {
|
|
||||||
if (getVideoPlayer().textTracks[i].mode == "showing") {
|
|
||||||
contentActiveSubtitle =[i + 1];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
contentSubtitles = [];
|
|
||||||
var videoSubtitles = videoData.data.subtitles; // Array of subtitles
|
|
||||||
if (typeof(videoSubtitles) != 'undefined' && videoData.config.downloads.subtitle) {
|
|
||||||
for (var i = 0; i < videoSubtitles.length; i++) {
|
|
||||||
subtitle = new chrome.cast.media.Track(i, chrome.cast.media.TrackType.TEXT);
|
|
||||||
subtitle.trackContentId = videoSubtitles[i].media_url;
|
|
||||||
subtitle.trackContentType = 'text/vtt';
|
|
||||||
subtitle.subtype = chrome.cast.media.TextTrackType.SUBTITLES;
|
|
||||||
subtitle.name = videoSubtitles[i].name;
|
|
||||||
subtitle.language = videoSubtitles[i].lang;
|
|
||||||
subtitle.customData = null;
|
|
||||||
contentSubtitles.push(subtitle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mediaInfo = new chrome.cast.media.MediaInfo(contentId, contentType); // Create MediaInfo var that contains url and content type
|
|
||||||
// mediaInfo.streamType = chrome.cast.media.StreamType.BUFFERED; // Set type of stream, BUFFERED, LIVE, OTHER
|
|
||||||
mediaInfo.metadata = new chrome.cast.media.GenericMediaMetadata(); // Create metadata var and add it to MediaInfo
|
|
||||||
mediaInfo.metadata.title = contentTitle.replace("&", "&"); // Set the video title
|
|
||||||
mediaInfo.metadata.images = [new chrome.cast.Image(contentImage)]; // Set the video thumbnail
|
|
||||||
// mediaInfo.textTrackStyle = new chrome.cast.media.TextTrackStyle();
|
|
||||||
mediaInfo.tracks = contentSubtitles;
|
|
||||||
|
|
||||||
var request = new chrome.cast.media.LoadRequest(mediaInfo); // Create request with the previously set MediaInfo.
|
|
||||||
// request.queueData = new chrome.cast.media.QueueData(); // See https://developers.google.com/cast/docs/reference/web_sender/chrome.cast.media.QueueData for playlist support.
|
|
||||||
request.currentTime = shiftCurrentTime(contentCurrentTime); // Set video start position based on the browser video position
|
|
||||||
request.activeTrackIds = contentActiveSubtitle; // Set active subtitle based on video player
|
|
||||||
// request.autoplay = false; // Set content to auto play, true by default
|
|
||||||
castSession.loadMedia(request).then(
|
|
||||||
function() {
|
|
||||||
castSuccessful();
|
|
||||||
},
|
|
||||||
function() {
|
|
||||||
castFailed(errorCode);
|
|
||||||
}
|
|
||||||
); // Send request to cast device
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function shiftCurrentTime(contentCurrentTime) { // Shift media back 3 seconds to prevent missing some of the content
|
|
||||||
if (contentCurrentTime > 5) {
|
|
||||||
return(contentCurrentTime - 3);
|
|
||||||
} else {
|
|
||||||
return(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function castSuccessful() {
|
|
||||||
// console.log('Cast Successful.');
|
|
||||||
getVideoPlayer().pause(); // Pause browser video on successful cast
|
|
||||||
}
|
|
||||||
|
|
||||||
function castFailed(errorCode) {
|
|
||||||
console.log('Error code: ' + errorCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
window['__onGCastApiAvailable'] = function(isAvailable) {
|
|
||||||
if (isAvailable) {
|
|
||||||
initializeCastApi();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,106 +0,0 @@
|
|||||||
/**
|
|
||||||
* Handle multi channel notifications
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
checkMessages()
|
|
||||||
|
|
||||||
// page map to notification status
|
|
||||||
const messageTypes = {
|
|
||||||
"download": ["message:download", "message:add", "message:rescan", "message:playlistscan"],
|
|
||||||
"channel": ["message:subchannel"],
|
|
||||||
"channel_id": ["message:playlistscan"],
|
|
||||||
"playlist": ["message:subplaylist"],
|
|
||||||
"setting": ["message:setting"]
|
|
||||||
}
|
|
||||||
|
|
||||||
// start to look for messages
|
|
||||||
function checkMessages() {
|
|
||||||
var notifications = document.getElementById("notifications");
|
|
||||||
if (notifications) {
|
|
||||||
var dataOrigin = notifications.getAttribute("data");
|
|
||||||
getMessages(dataOrigin);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// get messages for page on timer
|
|
||||||
function getMessages(dataOrigin) {
|
|
||||||
fetch('/progress/').then(response => {
|
|
||||||
return response.json();
|
|
||||||
}).then(responseData => {
|
|
||||||
var messages = buildMessage(responseData, dataOrigin);
|
|
||||||
if (messages.length > 0) {
|
|
||||||
// restart itself
|
|
||||||
setTimeout(function() {
|
|
||||||
getMessages(dataOrigin);
|
|
||||||
}, 3000);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// make div for all messages, return relevant
|
|
||||||
function buildMessage(responseData, dataOrigin) {
|
|
||||||
// filter relevan messages
|
|
||||||
var allMessages = responseData["messages"];
|
|
||||||
var messages = allMessages.filter(function(value) {
|
|
||||||
return messageTypes[dataOrigin].includes(value["status"])
|
|
||||||
}, dataOrigin);
|
|
||||||
// build divs
|
|
||||||
var notificationDiv = document.getElementById("notifications");
|
|
||||||
var nots = notificationDiv.childElementCount;
|
|
||||||
notificationDiv.innerHTML = "";
|
|
||||||
for (let i = 0; i < messages.length; i++) {
|
|
||||||
var messageData = messages[i];
|
|
||||||
var messageStatus = messageData["status"];
|
|
||||||
var messageBox = document.createElement("div");
|
|
||||||
var title = document.createElement("h3");
|
|
||||||
title.innerHTML = messageData["title"];
|
|
||||||
var message = document.createElement("p");
|
|
||||||
message.innerHTML = messageData["message"];
|
|
||||||
messageBox.appendChild(title);
|
|
||||||
messageBox.appendChild(message);
|
|
||||||
messageBox.classList.add(messageData["level"], "notification");
|
|
||||||
notificationDiv.appendChild(messageBox);
|
|
||||||
if (messageStatus === "message:download") {
|
|
||||||
checkDownloadIcons();
|
|
||||||
};
|
|
||||||
};
|
|
||||||
// reload page when no more notifications
|
|
||||||
if (nots > 0 && messages.length === 0) {
|
|
||||||
location.reload();
|
|
||||||
};
|
|
||||||
return messages
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if download icons are needed
|
|
||||||
function checkDownloadIcons() {
|
|
||||||
var iconBox = document.getElementById("downloadControl");
|
|
||||||
if (iconBox.childElementCount === 0) {
|
|
||||||
var downloadIcons = buildDownloadIcons();
|
|
||||||
iconBox.appendChild(downloadIcons);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// add dl control icons
|
|
||||||
function buildDownloadIcons() {
|
|
||||||
var downloadIcons = document.createElement('div');
|
|
||||||
downloadIcons.classList = 'dl-control-icons';
|
|
||||||
// stop icon
|
|
||||||
var stopIcon = document.createElement('img');
|
|
||||||
stopIcon.setAttribute('id', "stop-icon");
|
|
||||||
stopIcon.setAttribute('title', "Stop Download Queue");
|
|
||||||
stopIcon.setAttribute('src', "/static/img/icon-stop.svg");
|
|
||||||
stopIcon.setAttribute('alt', "stop icon");
|
|
||||||
stopIcon.setAttribute('onclick', 'stopQueue()');
|
|
||||||
// kill icon
|
|
||||||
var killIcon = document.createElement('img');
|
|
||||||
killIcon.setAttribute('id', "kill-icon");
|
|
||||||
killIcon.setAttribute('title', "Kill Download Queue");
|
|
||||||
killIcon.setAttribute('src', "/static/img/icon-close.svg");
|
|
||||||
killIcon.setAttribute('alt', "kill icon");
|
|
||||||
killIcon.setAttribute('onclick', 'killQueue()');
|
|
||||||
// stich together
|
|
||||||
downloadIcons.appendChild(stopIcon);
|
|
||||||
downloadIcons.appendChild(killIcon);
|
|
||||||
return downloadIcons
|
|
||||||
}
|
|
@ -1,912 +0,0 @@
|
|||||||
function sortChange(sortValue) {
|
|
||||||
var payload = JSON.stringify({ sort_order: sortValue });
|
|
||||||
sendPost(payload);
|
|
||||||
setTimeout(function () {
|
|
||||||
location.reload();
|
|
||||||
return false;
|
|
||||||
}, 500);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Updates video watch status when passed a video id and it's current state (ex if the video was unwatched but you want to mark it as watched you will pass "unwatched")
|
|
||||||
function updateVideoWatchStatus(input1, videoCurrentWatchStatus) {
|
|
||||||
if (videoCurrentWatchStatus) {
|
|
||||||
videoId = input1;
|
|
||||||
} else if (input1.getAttribute("data-id")) {
|
|
||||||
videoId = input1.getAttribute("data-id");
|
|
||||||
videoCurrentWatchStatus = input1.getAttribute("data-status");
|
|
||||||
}
|
|
||||||
|
|
||||||
postVideoProgress(videoId, 0); // Reset video progress on watched/unwatched;
|
|
||||||
removeProgressBar(videoId);
|
|
||||||
|
|
||||||
if (videoCurrentWatchStatus == "watched") {
|
|
||||||
var watchStatusIndicator = createWatchStatusIndicator(videoId, "unwatched");
|
|
||||||
var payload = JSON.stringify({ un_watched: videoId });
|
|
||||||
sendPost(payload);
|
|
||||||
} else if (videoCurrentWatchStatus == "unwatched") {
|
|
||||||
var watchStatusIndicator = createWatchStatusIndicator(videoId, "watched");
|
|
||||||
var payload = JSON.stringify({ watched: videoId });
|
|
||||||
sendPost(payload);
|
|
||||||
}
|
|
||||||
|
|
||||||
var watchButtons = document.getElementsByClassName("watch-button");
|
|
||||||
for (let i = 0; i < watchButtons.length; i++) {
|
|
||||||
if (watchButtons[i].getAttribute("data-id") == videoId) {
|
|
||||||
watchButtons[i].outerHTML = watchStatusIndicator;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Creates a watch status indicator when passed a video id and the videos watch status
|
|
||||||
function createWatchStatusIndicator(videoId, videoWatchStatus) {
|
|
||||||
if (videoWatchStatus == "watched") {
|
|
||||||
var seen = "seen";
|
|
||||||
var title = "Mark as unwatched";
|
|
||||||
} else if (videoWatchStatus == "unwatched") {
|
|
||||||
var seen = "unseen";
|
|
||||||
var title = "Mark as watched";
|
|
||||||
}
|
|
||||||
var watchStatusIndicator = `<img src="/static/img/icon-${seen}.svg" alt="${seen}-icon" data-id="${videoId}" data-status="${videoWatchStatus}" onclick="updateVideoWatchStatus(this)" class="watch-button" title="${title}">`;
|
|
||||||
return watchStatusIndicator;
|
|
||||||
}
|
|
||||||
|
|
||||||
// function isWatched(youtube_id) {
|
|
||||||
// var payload = JSON.stringify({'watched': youtube_id});
|
|
||||||
// sendPost(payload);
|
|
||||||
// var seenIcon = document.createElement('img');
|
|
||||||
// seenIcon.setAttribute('src', "/static/img/icon-seen.svg");
|
|
||||||
// seenIcon.setAttribute('alt', 'seen-icon');
|
|
||||||
// seenIcon.setAttribute('id', youtube_id);
|
|
||||||
// seenIcon.setAttribute('title', "Mark as unwatched");
|
|
||||||
// seenIcon.setAttribute('onclick', "isUnwatched(this.id)");
|
|
||||||
// seenIcon.classList = 'seen-icon';
|
|
||||||
// document.getElementById(youtube_id).replaceWith(seenIcon);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Removes the progress bar when passed a video id
|
|
||||||
function removeProgressBar(videoId) {
|
|
||||||
setProgressBar(videoId, 0, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
function isWatchedButton(button) {
|
|
||||||
youtube_id = button.getAttribute("data-id");
|
|
||||||
var payload = JSON.stringify({ watched: youtube_id });
|
|
||||||
button.remove();
|
|
||||||
sendPost(payload);
|
|
||||||
setTimeout(function () {
|
|
||||||
location.reload();
|
|
||||||
return false;
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
// function isUnwatched(youtube_id) {
|
|
||||||
// postVideoProgress(youtube_id, 0); // Reset video progress on unwatched;
|
|
||||||
// var payload = JSON.stringify({'un_watched': youtube_id});
|
|
||||||
// sendPost(payload);
|
|
||||||
// var unseenIcon = document.createElement('img');
|
|
||||||
// unseenIcon.setAttribute('src', "/static/img/icon-unseen.svg");
|
|
||||||
// unseenIcon.setAttribute('alt', 'unseen-icon');
|
|
||||||
// unseenIcon.setAttribute('id', youtube_id);
|
|
||||||
// unseenIcon.setAttribute('title', "Mark as watched");
|
|
||||||
// unseenIcon.setAttribute('onclick', "isWatched(this.id)");
|
|
||||||
// unseenIcon.classList = 'unseen-icon';
|
|
||||||
// document.getElementById(youtube_id).replaceWith(unseenIcon);
|
|
||||||
// }
|
|
||||||
|
|
||||||
function unsubscribe(id_unsub) {
|
|
||||||
var payload = JSON.stringify({ unsubscribe: id_unsub });
|
|
||||||
sendPost(payload);
|
|
||||||
var message = document.createElement("span");
|
|
||||||
message.innerText = "You are unsubscribed.";
|
|
||||||
document.getElementById(id_unsub).replaceWith(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
function subscribe(id_sub) {
|
|
||||||
var payload = JSON.stringify({ subscribe: id_sub });
|
|
||||||
sendPost(payload);
|
|
||||||
var message = document.createElement("span");
|
|
||||||
message.innerText = "You are subscribed.";
|
|
||||||
document.getElementById(id_sub).replaceWith(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
function changeView(image) {
|
|
||||||
var sourcePage = image.getAttribute("data-origin");
|
|
||||||
var newView = image.getAttribute("data-value");
|
|
||||||
var payload = JSON.stringify({ change_view: sourcePage + ":" + newView });
|
|
||||||
sendPost(payload);
|
|
||||||
setTimeout(function () {
|
|
||||||
location.reload();
|
|
||||||
return false;
|
|
||||||
}, 500);
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleCheckbox(checkbox) {
|
|
||||||
// pass checkbox id as key and checkbox.checked as value
|
|
||||||
var toggleId = checkbox.id;
|
|
||||||
var toggleVal = checkbox.checked;
|
|
||||||
var payloadDict = {};
|
|
||||||
payloadDict[toggleId] = toggleVal;
|
|
||||||
var payload = JSON.stringify(payloadDict);
|
|
||||||
sendPost(payload);
|
|
||||||
setTimeout(function () {
|
|
||||||
var currPage = window.location.pathname;
|
|
||||||
window.location.replace(currPage);
|
|
||||||
return false;
|
|
||||||
}, 500);
|
|
||||||
}
|
|
||||||
|
|
||||||
// download page buttons
|
|
||||||
function rescanPending() {
|
|
||||||
var payload = JSON.stringify({ rescan_pending: true });
|
|
||||||
animate("rescan-icon", "rotate-img");
|
|
||||||
sendPost(payload);
|
|
||||||
setTimeout(function () {
|
|
||||||
checkMessages();
|
|
||||||
}, 500);
|
|
||||||
}
|
|
||||||
|
|
||||||
function dlPending() {
|
|
||||||
var payload = JSON.stringify({ dl_pending: true });
|
|
||||||
animate("download-icon", "bounce-img");
|
|
||||||
sendPost(payload);
|
|
||||||
setTimeout(function () {
|
|
||||||
checkMessages();
|
|
||||||
}, 500);
|
|
||||||
}
|
|
||||||
|
|
||||||
function toIgnore(button) {
|
|
||||||
var youtube_id = button.getAttribute("data-id");
|
|
||||||
var payload = JSON.stringify({ ignore: youtube_id });
|
|
||||||
sendPost(payload);
|
|
||||||
document.getElementById("dl-" + youtube_id).remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
function downloadNow(button) {
|
|
||||||
var youtube_id = button.getAttribute("data-id");
|
|
||||||
var payload = JSON.stringify({ dlnow: youtube_id });
|
|
||||||
sendPost(payload);
|
|
||||||
document.getElementById(youtube_id).remove();
|
|
||||||
setTimeout(function () {
|
|
||||||
checkMessages();
|
|
||||||
}, 500);
|
|
||||||
}
|
|
||||||
|
|
||||||
function forgetIgnore(button) {
|
|
||||||
var youtube_id = button.getAttribute("data-id");
|
|
||||||
var payload = JSON.stringify({ forgetIgnore: youtube_id });
|
|
||||||
sendPost(payload);
|
|
||||||
document.getElementById("dl-" + youtube_id).remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
function addSingle(button) {
|
|
||||||
var youtube_id = button.getAttribute("data-id");
|
|
||||||
var payload = JSON.stringify({ addSingle: youtube_id });
|
|
||||||
sendPost(payload);
|
|
||||||
document.getElementById("dl-" + youtube_id).remove();
|
|
||||||
setTimeout(function () {
|
|
||||||
checkMessages();
|
|
||||||
}, 500);
|
|
||||||
}
|
|
||||||
|
|
||||||
function deleteQueue(button) {
|
|
||||||
var to_delete = button.getAttribute("data-id");
|
|
||||||
var payload = JSON.stringify({ deleteQueue: to_delete });
|
|
||||||
sendPost(payload);
|
|
||||||
setTimeout(function () {
|
|
||||||
location.reload();
|
|
||||||
return false;
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
function stopQueue() {
|
|
||||||
var payload = JSON.stringify({ queue: "stop" });
|
|
||||||
sendPost(payload);
|
|
||||||
document.getElementById("stop-icon").remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
function killQueue() {
|
|
||||||
var payload = JSON.stringify({ queue: "kill" });
|
|
||||||
sendPost(payload);
|
|
||||||
document.getElementById("kill-icon").remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
// settings page buttons
|
|
||||||
function manualImport() {
|
|
||||||
var payload = JSON.stringify({ "manual-import": true });
|
|
||||||
sendPost(payload);
|
|
||||||
// clear button
|
|
||||||
var message = document.createElement("p");
|
|
||||||
message.innerText = "processing import";
|
|
||||||
var toReplace = document.getElementById("manual-import");
|
|
||||||
toReplace.innerHTML = "";
|
|
||||||
toReplace.appendChild(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
function reEmbed() {
|
|
||||||
var payload = JSON.stringify({ "re-embed": true });
|
|
||||||
sendPost(payload);
|
|
||||||
// clear button
|
|
||||||
var message = document.createElement("p");
|
|
||||||
message.innerText = "processing thumbnails";
|
|
||||||
var toReplace = document.getElementById("re-embed");
|
|
||||||
toReplace.innerHTML = "";
|
|
||||||
toReplace.appendChild(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
function dbBackup() {
|
|
||||||
var payload = JSON.stringify({ "db-backup": true });
|
|
||||||
sendPost(payload);
|
|
||||||
// clear button
|
|
||||||
var message = document.createElement("p");
|
|
||||||
message.innerText = "backing up archive";
|
|
||||||
var toReplace = document.getElementById("db-backup");
|
|
||||||
toReplace.innerHTML = "";
|
|
||||||
toReplace.appendChild(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
function dbRestore(button) {
|
|
||||||
var fileName = button.getAttribute("data-id");
|
|
||||||
var payload = JSON.stringify({ "db-restore": fileName });
|
|
||||||
sendPost(payload);
|
|
||||||
// clear backup row
|
|
||||||
var message = document.createElement("p");
|
|
||||||
message.innerText = "restoring from backup";
|
|
||||||
var toReplace = document.getElementById(fileName);
|
|
||||||
toReplace.innerHTML = "";
|
|
||||||
toReplace.appendChild(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
function fsRescan() {
|
|
||||||
var payload = JSON.stringify({ "fs-rescan": true });
|
|
||||||
sendPost(payload);
|
|
||||||
// clear button
|
|
||||||
var message = document.createElement("p");
|
|
||||||
message.innerText = "File system scan in progress";
|
|
||||||
var toReplace = document.getElementById("fs-rescan");
|
|
||||||
toReplace.innerHTML = "";
|
|
||||||
toReplace.appendChild(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetToken() {
|
|
||||||
var payload = JSON.stringify({ "reset-token": true });
|
|
||||||
sendPost(payload);
|
|
||||||
var message = document.createElement("p");
|
|
||||||
message.innerText = "Token revoked";
|
|
||||||
document.getElementById("text-reveal").replaceWith(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
// delete from file system
|
|
||||||
function deleteConfirm() {
|
|
||||||
to_show = document.getElementById("delete-button");
|
|
||||||
document.getElementById("delete-item").style.display = "none";
|
|
||||||
to_show.style.display = "block";
|
|
||||||
}
|
|
||||||
|
|
||||||
function deleteVideo(button) {
|
|
||||||
var to_delete = button.getAttribute("data-id");
|
|
||||||
var to_redirect = button.getAttribute("data-redirect");
|
|
||||||
var payload = JSON.stringify({ "delete-video": to_delete });
|
|
||||||
sendPost(payload);
|
|
||||||
setTimeout(function () {
|
|
||||||
var redirect = "/channel/" + to_redirect;
|
|
||||||
window.location.replace(redirect);
|
|
||||||
return false;
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
function deleteChannel(button) {
|
|
||||||
var to_delete = button.getAttribute("data-id");
|
|
||||||
var payload = JSON.stringify({ "delete-channel": to_delete });
|
|
||||||
sendPost(payload);
|
|
||||||
setTimeout(function () {
|
|
||||||
window.location.replace("/channel/");
|
|
||||||
return false;
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
function deletePlaylist(button) {
|
|
||||||
var playlist_id = button.getAttribute("data-id");
|
|
||||||
var playlist_action = button.getAttribute("data-action");
|
|
||||||
var payload = JSON.stringify({
|
|
||||||
"delete-playlist": {
|
|
||||||
"playlist-id": playlist_id,
|
|
||||||
"playlist-action": playlist_action,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
sendPost(payload);
|
|
||||||
setTimeout(function () {
|
|
||||||
window.location.replace("/playlist/");
|
|
||||||
return false;
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
function cancelDelete() {
|
|
||||||
document.getElementById("delete-button").style.display = "none";
|
|
||||||
document.getElementById("delete-item").style.display = "block";
|
|
||||||
}
|
|
||||||
|
|
||||||
// player
|
|
||||||
function createPlayer(button) {
|
|
||||||
var videoId = button.getAttribute("data-id");
|
|
||||||
var videoData = getVideoData(videoId);
|
|
||||||
var videoProgress = getVideoProgress(videoId).position;
|
|
||||||
var videoName = videoData.data.title;
|
|
||||||
|
|
||||||
var videoTag = createVideoTag(videoData, videoProgress);
|
|
||||||
|
|
||||||
var playlist = "";
|
|
||||||
var videoPlaylists = videoData.data.playlist; // Array of playlists the video is in
|
|
||||||
if (typeof videoPlaylists != "undefined") {
|
|
||||||
var subbedPlaylists = getSubbedPlaylists(videoPlaylists); // Array of playlist the video is in that are subscribed
|
|
||||||
if (subbedPlaylists.length != 0) {
|
|
||||||
var playlistData = getPlaylistData(subbedPlaylists[0]); // Playlist data for first subscribed playlist
|
|
||||||
var playlistId = playlistData.playlist_id;
|
|
||||||
var playlistName = playlistData.playlist_name;
|
|
||||||
var playlist = `<h5><a href="/playlist/${playlistId}/">${playlistName}</a></h5>`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var videoViews = formatNumbers(videoData.data.stats.view_count);
|
|
||||||
|
|
||||||
var channelId = videoData.data.channel.channel_id;
|
|
||||||
var channelName = videoData.data.channel.channel_name;
|
|
||||||
|
|
||||||
removePlayer();
|
|
||||||
// document.getElementById(videoId).outerHTML = ''; // Remove watch indicator from video info
|
|
||||||
|
|
||||||
// If cast integration is enabled create cast button
|
|
||||||
var castButton = "";
|
|
||||||
if (videoData.config.application.enable_cast) {
|
|
||||||
var castButton = `<google-cast-launcher id="castbutton"></google-cast-launcher>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Watched indicator
|
|
||||||
if (videoData.data.player.watched) {
|
|
||||||
var watchStatusIndicator = createWatchStatusIndicator(videoId, "watched");
|
|
||||||
} else {
|
|
||||||
var watchStatusIndicator = createWatchStatusIndicator(videoId, "unwatched");
|
|
||||||
}
|
|
||||||
|
|
||||||
var playerStats = `<div class="thumb-icon player-stats"><img src="/static/img/icon-eye.svg" alt="views icon"><span>${videoViews}</span>`;
|
|
||||||
if (videoData.data.stats.like_count) {
|
|
||||||
var likes = formatNumbers(videoData.data.stats.like_count);
|
|
||||||
playerStats += `<span>|</span><img src="/static/img/icon-thumb.svg" alt="thumbs-up"><span>${likes}</span>`;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
videoData.data.stats.dislike_count &&
|
|
||||||
videoData.config.downloads.integrate_ryd
|
|
||||||
) {
|
|
||||||
var dislikes = formatNumbers(videoData.data.stats.dislike_count);
|
|
||||||
playerStats += `<span>|</span><img class="dislike" src="/static/img/icon-thumb.svg" alt="thumbs-down"><span>${dislikes}</span>`;
|
|
||||||
}
|
|
||||||
playerStats += "</div>";
|
|
||||||
|
|
||||||
const markup = `
|
|
||||||
<div class="video-player" data-id="${videoId}">
|
|
||||||
${videoTag}
|
|
||||||
<div class="player-title boxed-content">
|
|
||||||
<img class="close-button" src="/static/img/icon-close.svg" alt="close-icon" data="${videoId}" onclick="removePlayer()" title="Close player">
|
|
||||||
${watchStatusIndicator}
|
|
||||||
${castButton}
|
|
||||||
${playerStats}
|
|
||||||
<div class="player-channel-playlist">
|
|
||||||
<h3><a href="/channel/${channelId}/">${channelName}</a></h3>
|
|
||||||
${playlist}
|
|
||||||
</div>
|
|
||||||
<a href="/video/${videoId}/"><h2 id="video-title">${videoName}</h2></a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
const divPlayer = document.getElementById("player");
|
|
||||||
divPlayer.innerHTML = markup;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add video tag to video page when passed a video id, function loaded on page load `video.html (115-117)`
|
|
||||||
function insertVideoTag(videoData, videoProgress) {
|
|
||||||
var videoTag = createVideoTag(videoData, videoProgress);
|
|
||||||
var videoMain = document.getElementsByClassName("video-main");
|
|
||||||
videoMain[0].innerHTML = videoTag;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generates a video tag with subtitles when passed videoData and videoProgress.
|
|
||||||
function createVideoTag(videoData, videoProgress) {
|
|
||||||
var videoId = videoData.data.youtube_id;
|
|
||||||
var videoUrl = videoData.data.media_url;
|
|
||||||
var videoThumbUrl = videoData.data.vid_thumb_url;
|
|
||||||
var subtitles = "";
|
|
||||||
var videoSubtitles = videoData.data.subtitles; // Array of subtitles
|
|
||||||
if (
|
|
||||||
typeof videoSubtitles != "undefined" &&
|
|
||||||
videoData.config.downloads.subtitle
|
|
||||||
) {
|
|
||||||
for (var i = 0; i < videoSubtitles.length; i++) {
|
|
||||||
let label = videoSubtitles[i].name;
|
|
||||||
if (videoSubtitles[i].source == "auto") {
|
|
||||||
label += " - auto";
|
|
||||||
}
|
|
||||||
subtitles += `<track label="${label}" kind="subtitles" srclang="${videoSubtitles[i].lang}" src="${videoSubtitles[i].media_url}">`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var videoTag = `
|
|
||||||
<video poster="${videoThumbUrl}" ontimeupdate="onVideoProgress()" onpause="onVideoPause()" onended="onVideoEnded()" controls autoplay width="100%" playsinline id="video-item">
|
|
||||||
<source src="${videoUrl}#t=${videoProgress}" type="video/mp4" id="video-source" videoid="${videoId}">
|
|
||||||
${subtitles}
|
|
||||||
</video>
|
|
||||||
`;
|
|
||||||
return videoTag;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Gets video tag
|
|
||||||
function getVideoPlayer() {
|
|
||||||
var videoElement = document.getElementById("video-item");
|
|
||||||
return videoElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Gets the video source tag
|
|
||||||
function getVideoPlayerVideoSource() {
|
|
||||||
var videoPlayerVideoSource = document.getElementById("video-source");
|
|
||||||
return videoPlayerVideoSource;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Gets the current progress of the video currently in the player
|
|
||||||
function getVideoPlayerCurrentTime() {
|
|
||||||
var videoElement = getVideoPlayer();
|
|
||||||
if (videoElement != null) {
|
|
||||||
return videoElement.currentTime;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Gets the video id of the video currently in the player
|
|
||||||
function getVideoPlayerVideoId() {
|
|
||||||
var videoPlayerVideoSource = getVideoPlayerVideoSource();
|
|
||||||
if (videoPlayerVideoSource != null) {
|
|
||||||
return videoPlayerVideoSource.getAttribute("videoid");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Gets the duration of the video currently in the player
|
|
||||||
function getVideoPlayerDuration() {
|
|
||||||
var videoElement = getVideoPlayer();
|
|
||||||
if (videoElement != null) {
|
|
||||||
return videoElement.duration;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Gets current watch status of video based on watch button
|
|
||||||
function getVideoPlayerWatchStatus() {
|
|
||||||
var videoId = getVideoPlayerVideoId();
|
|
||||||
var watched = false;
|
|
||||||
|
|
||||||
var watchButtons = document.getElementsByClassName("watch-button");
|
|
||||||
for (let i = 0; i < watchButtons.length; i++) {
|
|
||||||
if (
|
|
||||||
watchButtons[i].getAttribute("data-id") == videoId &&
|
|
||||||
watchButtons[i].getAttribute("data-status") == "watched"
|
|
||||||
) {
|
|
||||||
watched = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return watched;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Runs on video playback, marks video as watched if video gets to 90% or higher, sends position to api
|
|
||||||
function onVideoProgress() {
|
|
||||||
var videoId = getVideoPlayerVideoId();
|
|
||||||
var currentTime = getVideoPlayerCurrentTime();
|
|
||||||
var duration = getVideoPlayerDuration();
|
|
||||||
if ((currentTime % 10).toFixed(1) <= 0.2) {
|
|
||||||
// Check progress every 10 seconds or else progress is checked a few times a second
|
|
||||||
postVideoProgress(videoId, currentTime);
|
|
||||||
if (!getVideoPlayerWatchStatus()) {
|
|
||||||
// Check if video is already marked as watched
|
|
||||||
if (watchedThreshold(currentTime, duration)) {
|
|
||||||
updateVideoWatchStatus(videoId, "unwatched");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Runs on video end, marks video as watched
|
|
||||||
function onVideoEnded() {
|
|
||||||
var videoId = getVideoPlayerVideoId();
|
|
||||||
if (!getVideoPlayerWatchStatus()) {
|
|
||||||
// Check if video is already marked as watched
|
|
||||||
updateVideoWatchStatus(videoId, "unwatched");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function watchedThreshold(currentTime, duration) {
|
|
||||||
var watched = false;
|
|
||||||
if (duration <= 1800) {
|
|
||||||
// If video is less than 30 min
|
|
||||||
if (currentTime / duration >= 0.9) {
|
|
||||||
// Mark as watched at 90%
|
|
||||||
var watched = true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// If video is more than 30 min
|
|
||||||
if (currentTime >= duration - 120) {
|
|
||||||
// Mark as watched if there is two minutes left
|
|
||||||
var watched = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return watched;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Runs on video pause. Sends current position.
|
|
||||||
function onVideoPause() {
|
|
||||||
var videoId = getVideoPlayerVideoId();
|
|
||||||
var currentTime = getVideoPlayerCurrentTime();
|
|
||||||
postVideoProgress(videoId, currentTime);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Format numbers for frontend
|
|
||||||
function formatNumbers(number) {
|
|
||||||
var numberUnformatted = parseFloat(number);
|
|
||||||
if (numberUnformatted > 999999999) {
|
|
||||||
var numberFormatted =
|
|
||||||
(numberUnformatted / 1000000000).toFixed(1).toString() + "B";
|
|
||||||
} else if (numberUnformatted > 999999) {
|
|
||||||
var numberFormatted =
|
|
||||||
(numberUnformatted / 1000000).toFixed(1).toString() + "M";
|
|
||||||
} else if (numberUnformatted > 999) {
|
|
||||||
var numberFormatted =
|
|
||||||
(numberUnformatted / 1000).toFixed(1).toString() + "K";
|
|
||||||
} else {
|
|
||||||
var numberFormatted = numberUnformatted;
|
|
||||||
}
|
|
||||||
return numberFormatted;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Gets video data when passed video ID
|
|
||||||
function getVideoData(videoId) {
|
|
||||||
var apiEndpoint = "/api/video/" + videoId + "/";
|
|
||||||
var videoData = apiRequest(apiEndpoint, "GET");
|
|
||||||
return videoData;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Gets channel data when passed channel ID
|
|
||||||
function getChannelData(channelId) {
|
|
||||||
var apiEndpoint = "/api/channel/" + channelId + "/";
|
|
||||||
var channelData = apiRequest(apiEndpoint, "GET");
|
|
||||||
return channelData.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Gets playlist data when passed playlist ID
|
|
||||||
function getPlaylistData(playlistId) {
|
|
||||||
var apiEndpoint = "/api/playlist/" + playlistId + "/";
|
|
||||||
var playlistData = apiRequest(apiEndpoint, "GET");
|
|
||||||
return playlistData.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get video progress data when passed video ID
|
|
||||||
function getVideoProgress(videoId) {
|
|
||||||
var apiEndpoint = "/api/video/" + videoId + "/progress/";
|
|
||||||
var videoProgress = apiRequest(apiEndpoint, "GET");
|
|
||||||
return videoProgress;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Given an array of playlist ids it returns an array of subbed playlist ids from that list
|
|
||||||
function getSubbedPlaylists(videoPlaylists) {
|
|
||||||
var subbedPlaylists = [];
|
|
||||||
for (var i = 0; i < videoPlaylists.length; i++) {
|
|
||||||
if (getPlaylistData(videoPlaylists[i]).playlist_subscribed) {
|
|
||||||
subbedPlaylists.push(videoPlaylists[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return subbedPlaylists;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send video position when given video id and progress in seconds
|
|
||||||
function postVideoProgress(videoId, videoProgress) {
|
|
||||||
var apiEndpoint = "/api/video/" + videoId + "/progress/";
|
|
||||||
var duartion = getVideoPlayerDuration();
|
|
||||||
if (!isNaN(videoProgress) && duartion != "undefined") {
|
|
||||||
var data = {
|
|
||||||
position: videoProgress,
|
|
||||||
};
|
|
||||||
if (videoProgress == 0) {
|
|
||||||
apiRequest(apiEndpoint, "DELETE");
|
|
||||||
// console.log("Deleting Video Progress for Video ID: " + videoId + ", Progress: " + videoProgress);
|
|
||||||
} else if (!getVideoPlayerWatchStatus()) {
|
|
||||||
apiRequest(apiEndpoint, "POST", data);
|
|
||||||
// console.log("Saving Video Progress for Video ID: " + videoId + ", Progress: " + videoProgress);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Makes api requests when passed an endpoint and method ("GET", "POST", "DELETE")
|
|
||||||
function apiRequest(apiEndpoint, method, data) {
|
|
||||||
const xhttp = new XMLHttpRequest();
|
|
||||||
var sessionToken = getCookie("sessionid");
|
|
||||||
xhttp.open(method, apiEndpoint, false);
|
|
||||||
xhttp.setRequestHeader("X-CSRFToken", getCookie("csrftoken")); // Used for video progress POST requests
|
|
||||||
xhttp.setRequestHeader("Authorization", "Token " + sessionToken);
|
|
||||||
xhttp.setRequestHeader("Content-Type", "application/json");
|
|
||||||
xhttp.send(JSON.stringify(data));
|
|
||||||
return JSON.parse(xhttp.responseText);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Gets origin URL
|
|
||||||
function getURL() {
|
|
||||||
return window.location.origin;
|
|
||||||
}
|
|
||||||
|
|
||||||
function removePlayer() {
|
|
||||||
var currentTime = getVideoPlayerCurrentTime();
|
|
||||||
var duration = getVideoPlayerDuration();
|
|
||||||
var videoId = getVideoPlayerVideoId();
|
|
||||||
postVideoProgress(videoId, currentTime);
|
|
||||||
setProgressBar(videoId, currentTime, duration);
|
|
||||||
var playerElement = document.getElementById("player");
|
|
||||||
if (playerElement.hasChildNodes()) {
|
|
||||||
var youtubeId = playerElement.childNodes[1].getAttribute("data-id");
|
|
||||||
var playedStatus = document.createDocumentFragment();
|
|
||||||
var playedBox = document.getElementById(youtubeId);
|
|
||||||
if (playedBox) {
|
|
||||||
playedStatus.appendChild(playedBox);
|
|
||||||
}
|
|
||||||
playerElement.innerHTML = "";
|
|
||||||
// append played status
|
|
||||||
var videoInfo = document.getElementById("video-info-" + youtubeId);
|
|
||||||
videoInfo.insertBefore(playedStatus, videoInfo.firstChild);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sets the progress bar when passed a video id, video progress and video duration
|
|
||||||
function setProgressBar(videoId, currentTime, duration) {
|
|
||||||
var progressBarWidth = (currentTime / duration) * 100 + "%";
|
|
||||||
var progressBars = document.getElementsByClassName("video-progress-bar");
|
|
||||||
for (let i = 0; i < progressBars.length; i++) {
|
|
||||||
if (progressBars[i].id == "progress-" + videoId) {
|
|
||||||
if (!getVideoPlayerWatchStatus()) {
|
|
||||||
progressBars[i].style.width = progressBarWidth;
|
|
||||||
} else {
|
|
||||||
progressBars[i].style.width = "0%";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// progressBar = document.getElementById("progress-" + videoId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// multi search form
|
|
||||||
function searchMulti(query) {
|
|
||||||
if (query.length > 1) {
|
|
||||||
var payload = JSON.stringify({ multi_search: query });
|
|
||||||
var http = new XMLHttpRequest();
|
|
||||||
http.onreadystatechange = function () {
|
|
||||||
if (http.readyState === 4) {
|
|
||||||
allResults = JSON.parse(http.response).results;
|
|
||||||
populateMultiSearchResults(allResults);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
http.open("POST", "/process/", true);
|
|
||||||
http.setRequestHeader("X-CSRFToken", getCookie("csrftoken"));
|
|
||||||
http.setRequestHeader("Content-type", "application/json");
|
|
||||||
http.send(payload);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getViewDefaults(view) {
|
|
||||||
var defaultView = document.getElementById("id_" + view).value;
|
|
||||||
return defaultView;
|
|
||||||
}
|
|
||||||
|
|
||||||
function populateMultiSearchResults(allResults) {
|
|
||||||
// videos
|
|
||||||
var defaultVideo = getViewDefaults("home");
|
|
||||||
var allVideos = allResults.video_results;
|
|
||||||
var videoBox = document.getElementById("video-results");
|
|
||||||
videoBox.innerHTML = "";
|
|
||||||
for (let index = 0; index < allVideos.length; index++) {
|
|
||||||
const video = allVideos[index].source;
|
|
||||||
const videoDiv = createVideo(video, defaultVideo);
|
|
||||||
videoBox.appendChild(videoDiv);
|
|
||||||
}
|
|
||||||
// channels
|
|
||||||
var defaultChannel = getViewDefaults("channel");
|
|
||||||
var allChannels = allResults.channel_results;
|
|
||||||
var channelBox = document.getElementById("channel-results");
|
|
||||||
channelBox.innerHTML = "";
|
|
||||||
for (let index = 0; index < allChannels.length; index++) {
|
|
||||||
const channel = allChannels[index].source;
|
|
||||||
const channelDiv = createChannel(channel, defaultChannel);
|
|
||||||
channelBox.appendChild(channelDiv);
|
|
||||||
}
|
|
||||||
// playlists
|
|
||||||
var defaultPlaylist = getViewDefaults("playlist");
|
|
||||||
var allPlaylists = allResults.playlist_results;
|
|
||||||
var playlistBox = document.getElementById("playlist-results");
|
|
||||||
playlistBox.innerHTML = "";
|
|
||||||
for (let index = 0; index < allPlaylists.length; index++) {
|
|
||||||
const playlist = allPlaylists[index].source;
|
|
||||||
const playlistDiv = createPlaylist(playlist, defaultPlaylist);
|
|
||||||
playlistBox.appendChild(playlistDiv);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function createVideo(video, viewStyle) {
|
|
||||||
// create video item div from template
|
|
||||||
const videoId = video.youtube_id;
|
|
||||||
const mediaUrl = video.media_url;
|
|
||||||
const thumbUrl = "/cache/" + video.vid_thumb_url;
|
|
||||||
const videoTitle = video.title;
|
|
||||||
const videoPublished = video.published;
|
|
||||||
const videoDuration = video.player.duration_str;
|
|
||||||
if (video.player.watched) {
|
|
||||||
var watchStatusIndicator = createWatchStatusIndicator(videoId, "watched");
|
|
||||||
} else {
|
|
||||||
var watchStatusIndicator = createWatchStatusIndicator(videoId, "unwatched");
|
|
||||||
}
|
|
||||||
const channelId = video.channel.channel_id;
|
|
||||||
const channelName = video.channel.channel_name;
|
|
||||||
// build markup
|
|
||||||
const markup = `
|
|
||||||
<a href="#player" data-src="/media/${mediaUrl}" data-thumb="${thumbUrl}" data-title="${videoTitle}" data-channel="${channelName}" data-channel-id="${channelId}" data-id="${videoId}" onclick="createPlayer(this)">
|
|
||||||
<div class="video-thumb-wrap ${viewStyle}">
|
|
||||||
<div class="video-thumb">
|
|
||||||
<img src="${thumbUrl}" alt="video-thumb">
|
|
||||||
</div>
|
|
||||||
<div class="video-play">
|
|
||||||
<img src="/static/img/icon-play.svg" alt="play-icon">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
<div class="video-desc ${viewStyle}">
|
|
||||||
<div class="video-desc-player" id="video-info-${videoId}">
|
|
||||||
${watchStatusIndicator}
|
|
||||||
<span>${videoPublished} | ${videoDuration}</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<a href="/channel/${channelId}/"><h3>${channelName}</h3></a>
|
|
||||||
<a class="video-more" href="/video/${videoId}/"><h2>${videoTitle}</h2></a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
const videoDiv = document.createElement("div");
|
|
||||||
videoDiv.setAttribute("class", "video-item " + viewStyle);
|
|
||||||
videoDiv.innerHTML = markup;
|
|
||||||
return videoDiv;
|
|
||||||
}
|
|
||||||
|
|
||||||
function createChannel(channel, viewStyle) {
|
|
||||||
// create channel item div from template
|
|
||||||
const channelId = channel.channel_id;
|
|
||||||
const channelName = channel.channel_name;
|
|
||||||
const channelSubs = channel.channel_subs;
|
|
||||||
const channelLastRefresh = channel.channel_last_refresh;
|
|
||||||
if (channel.channel_subscribed) {
|
|
||||||
var button = `<button class="unsubscribe" type="button" id="${channelId}" onclick="unsubscribe(this.id)" title="Unsubscribe from ${channelName}">Unsubscribe</button>`;
|
|
||||||
} else {
|
|
||||||
var button = `<button type="button" id="${channelId}" onclick="subscribe(this.id)" title="Subscribe to ${channelName}">Subscribe</button>`;
|
|
||||||
}
|
|
||||||
// build markup
|
|
||||||
const markup = `
|
|
||||||
<div class="channel-banner ${viewStyle}">
|
|
||||||
<a href="/channel/${channelId}/">
|
|
||||||
<img src="/cache/channels/${channelId}_banner.jpg" alt="${channelId}-banner">
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="info-box info-box-2 ${viewStyle}">
|
|
||||||
<div class="info-box-item">
|
|
||||||
<div class="round-img">
|
|
||||||
<a href="/channel/${channelId}/">
|
|
||||||
<img src="/cache/channels/${channelId}_thumb.jpg" alt="channel-thumb">
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3><a href="/channel/${channelId}/">${channelName}</a></h3>
|
|
||||||
<p>Subscribers: ${channelSubs}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="info-box-item">
|
|
||||||
<div>
|
|
||||||
<p>Last refreshed: ${channelLastRefresh}</p>
|
|
||||||
${button}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
const channelDiv = document.createElement("div");
|
|
||||||
channelDiv.setAttribute("class", "channel-item " + viewStyle);
|
|
||||||
channelDiv.innerHTML = markup;
|
|
||||||
return channelDiv;
|
|
||||||
}
|
|
||||||
|
|
||||||
function createPlaylist(playlist, viewStyle) {
|
|
||||||
// create playlist item div from template
|
|
||||||
const playlistId = playlist.playlist_id;
|
|
||||||
const playlistName = playlist.playlist_name;
|
|
||||||
const playlistChannelId = playlist.playlist_channel_id;
|
|
||||||
const playlistChannel = playlist.playlist_channel;
|
|
||||||
const playlistLastRefresh = playlist.playlist_last_refresh;
|
|
||||||
if (playlist.playlist_subscribed) {
|
|
||||||
var button = `<button class="unsubscribe" type="button" id="${playlistId}" onclick="unsubscribe(this.id)" title="Unsubscribe from ${playlistName}">Unsubscribe</button>`;
|
|
||||||
} else {
|
|
||||||
var button = `<button type="button" id="${playlistId}" onclick="subscribe(this.id)" title="Subscribe to ${playlistName}">Subscribe</button>`;
|
|
||||||
}
|
|
||||||
const markup = `
|
|
||||||
<div class="playlist-thumbnail">
|
|
||||||
<a href="/playlist/${playlistId}/">
|
|
||||||
<img src="/cache/playlists/${playlistId}.jpg" alt="${playlistId}-thumbnail">
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="playlist-desc ${viewStyle}">
|
|
||||||
<a href="/channel/${playlistChannelId}/"><h3>${playlistChannel}</h3></a>
|
|
||||||
<a href="/playlist/${playlistId}/"><h2>${playlistName}</h2></a>
|
|
||||||
<p>Last refreshed: ${playlistLastRefresh}</p>
|
|
||||||
${button}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
const playlistDiv = document.createElement("div");
|
|
||||||
playlistDiv.setAttribute("class", "playlist-item " + viewStyle);
|
|
||||||
playlistDiv.innerHTML = markup;
|
|
||||||
return playlistDiv;
|
|
||||||
}
|
|
||||||
|
|
||||||
// generic
|
|
||||||
function sendPost(payload) {
|
|
||||||
var http = new XMLHttpRequest();
|
|
||||||
http.open("POST", "/process/", true);
|
|
||||||
http.setRequestHeader("X-CSRFToken", getCookie("csrftoken"));
|
|
||||||
http.setRequestHeader("Content-type", "application/json");
|
|
||||||
http.send(payload);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCookie(c_name) {
|
|
||||||
if (document.cookie.length > 0) {
|
|
||||||
c_start = document.cookie.indexOf(c_name + "=");
|
|
||||||
if (c_start != -1) {
|
|
||||||
c_start = c_start + c_name.length + 1;
|
|
||||||
c_end = document.cookie.indexOf(";", c_start);
|
|
||||||
if (c_end == -1) c_end = document.cookie.length;
|
|
||||||
return unescape(document.cookie.substring(c_start, c_end));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
// animations
|
|
||||||
function textReveal() {
|
|
||||||
var textBox = document.getElementById("text-reveal");
|
|
||||||
var button = document.getElementById("text-reveal-button");
|
|
||||||
var textBoxHeight = textBox.style.height;
|
|
||||||
if (textBoxHeight === "unset") {
|
|
||||||
textBox.style.height = "0px";
|
|
||||||
button.innerText = "Show";
|
|
||||||
} else {
|
|
||||||
textBox.style.height = "unset";
|
|
||||||
button.innerText = "Hide";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function showForm() {
|
|
||||||
var formElement = document.getElementById("hidden-form");
|
|
||||||
var displayStyle = formElement.style.display;
|
|
||||||
if (displayStyle === "") {
|
|
||||||
formElement.style.display = "block";
|
|
||||||
} else {
|
|
||||||
formElement.style.display = "";
|
|
||||||
}
|
|
||||||
animate("animate-icon", "pulse-img");
|
|
||||||
}
|
|
||||||
|
|
||||||
function showOverwrite() {
|
|
||||||
var overwriteDiv = document.getElementById("overwrite-form");
|
|
||||||
if (overwriteDiv.classList.contains("hidden-overwrite")) {
|
|
||||||
overwriteDiv.classList.remove("hidden-overwrite");
|
|
||||||
} else {
|
|
||||||
overwriteDiv.classList.add("hidden-overwrite");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function animate(elementId, animationClass) {
|
|
||||||
var toAnimate = document.getElementById(elementId);
|
|
||||||
if (toAnimate.className !== animationClass) {
|
|
||||||
toAnimate.className = animationClass;
|
|
||||||
} else {
|
|
||||||
toAnimate.classList.remove(animationClass);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,4 +0,0 @@
|
|||||||
<svg width="283" height="64" viewBox="0 0 283 64" fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M141.04 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.46 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM248.72 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.45 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM200.24 34c0 6 3.92 10 10 10 4.12 0 7.21-1.87 8.8-4.92l7.68 4.43c-3.18 5.3-9.14 8.49-16.48 8.49-11.05 0-19-7.2-19-18s7.96-18 19-18c7.34 0 13.29 3.19 16.48 8.49l-7.68 4.43c-1.59-3.05-4.68-4.92-8.8-4.92-6.07 0-10 4-10 10zm82.48-29v46h-9V5h9zM36.95 0L73.9 64H0L36.95 0zm92.38 5l-27.71 48L73.91 5H84.3l17.32 30 17.32-30h10.39zm58.91 12v9.69c-1-.29-2.06-.49-3.2-.49-5.81 0-10 4-10 10V51h-9V17h9v9.2c0-5.08 5.91-9.2 13.2-9.2z" fill="#000"/>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 1.1 KiB |
@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": ["config:base"],
|
|
||||||
"dependencyDashboardApproval": true
|
|
||||||
}
|
|
39
run.sh
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# startup script inside the container for tubearchivist
|
||||||
|
|
||||||
|
# check environment
|
||||||
|
if [[ -z "$ELASTIC_USER" ]]; then
|
||||||
|
export ELASTIC_USER=elastic
|
||||||
|
fi
|
||||||
|
|
||||||
|
ENV_VARS=("TA_USERNAME" "TA_PASSWORD" "ELASTIC_PASSWORD" "ELASTIC_USER")
|
||||||
|
for each in "${ENV_VARS[@]}"; do
|
||||||
|
if ! [[ -v $each ]]; then
|
||||||
|
echo "missing environment variable $each"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# wait for elasticsearch
|
||||||
|
counter=0
|
||||||
|
until curl -u "$ELASTIC_USER":"$ELASTIC_PASSWORD" "$ES_URL" -fs; do
|
||||||
|
echo "waiting for elastic search to start"
|
||||||
|
counter=$((counter+1))
|
||||||
|
if [[ $counter -eq 12 ]]; then
|
||||||
|
# fail after 2 min
|
||||||
|
echo "failed to connect to elastic search, exiting..."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
sleep 10
|
||||||
|
done
|
||||||
|
|
||||||
|
# start python application
|
||||||
|
python manage.py makemigrations
|
||||||
|
python manage.py migrate
|
||||||
|
export DJANGO_SUPERUSER_PASSWORD=$TA_PASSWORD && \
|
||||||
|
python manage.py createsuperuser --noinput --name "$TA_USERNAME"
|
||||||
|
|
||||||
|
python manage.py collectstatic --noinput -c
|
||||||
|
nginx &
|
||||||
|
celery -A home.tasks worker --loglevel=INFO &
|
||||||
|
uwsgi --ini uwsgi.ini
|
@ -1,3 +0,0 @@
|
|||||||
export const BoxedContent: React.FC = ({ children }) => (
|
|
||||||
<div className="boxed-content">{children}</div>
|
|
||||||
);
|
|
@ -1,52 +0,0 @@
|
|||||||
import Head from "next/head";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TODO: Dynamically get the title
|
|
||||||
* TODO: NextJS recommended pattern for SEO
|
|
||||||
*/
|
|
||||||
export const CustomHead = ({ title }: { title?: string }) => {
|
|
||||||
return (
|
|
||||||
<Head>
|
|
||||||
<meta charSet="UTF-8" />
|
|
||||||
<meta httpEquiv="X-UA-Compatible" content="IE=edge" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<link
|
|
||||||
rel="apple-touch-icon"
|
|
||||||
sizes="180x180"
|
|
||||||
href="/favicon/apple-touch-icon.png"
|
|
||||||
/>
|
|
||||||
<link
|
|
||||||
rel="icon"
|
|
||||||
type="image/png"
|
|
||||||
sizes="32x32"
|
|
||||||
href="/favicon/favicon-32x32.png"
|
|
||||||
/>
|
|
||||||
<link
|
|
||||||
rel="icon"
|
|
||||||
type="image/png"
|
|
||||||
sizes="16x16"
|
|
||||||
href="/favicon/favicon-16x16.png"
|
|
||||||
/>
|
|
||||||
<link rel="manifest" href="/favicon/site.webmanifest" />
|
|
||||||
<link
|
|
||||||
rel="mask-icon"
|
|
||||||
href="/favicon/safari-pinned-tab.svg"
|
|
||||||
color="#01202e"
|
|
||||||
/>
|
|
||||||
<link rel="shortcut icon" href="/favicon/favicon.ico" />
|
|
||||||
<meta name="apple-mobile-web-app-title" content="TubeArchivist" />
|
|
||||||
<meta name="application-name" content="TubeArchivist" />
|
|
||||||
<meta name="msapplication-TileColor" content="#01202e" />
|
|
||||||
<meta name="msapplication-config" content="/favicon/browserconfig.xml" />
|
|
||||||
<meta name="theme-color" content="#01202e" />
|
|
||||||
|
|
||||||
{title ? <title>TA | {title}</title> : <title>TubeArchivist</title>}
|
|
||||||
|
|
||||||
{/* {% if colors == "dark" %} */}
|
|
||||||
{/* <link rel="stylesheet" href="/css/dark.css" /> */}
|
|
||||||
{/* {% else %} */}
|
|
||||||
{/* <link rel="stylesheet" href="/css/light.css" /> */}
|
|
||||||
{/* {% endif %} */}
|
|
||||||
</Head>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,34 +0,0 @@
|
|||||||
export const Footer = () => (
|
|
||||||
<div className="footer">
|
|
||||||
<div className="boxed-content">
|
|
||||||
<span>© 2021 - {new Date().getFullYear()} TubeArchivist v0.1.3 </span>
|
|
||||||
<span>
|
|
||||||
<a href="{% url 'about' %}">About</a> |{" "}
|
|
||||||
<a
|
|
||||||
href="https://github.com/bbilly1/tubearchivist"
|
|
||||||
rel="noreferrer"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
GitHub
|
|
||||||
</a>{" "}
|
|
||||||
|{" "}
|
|
||||||
<a
|
|
||||||
href="https://hub.docker.com/r/bbilly1/tubearchivist"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
Docker Hub
|
|
||||||
</a>{" "}
|
|
||||||
|{" "}
|
|
||||||
<a
|
|
||||||
href="https://discord.gg/AFwz8nE7BK"
|
|
||||||
rel="noreferrer"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
Discord
|
|
||||||
</a>{" "}
|
|
||||||
| <a href="https://www.reddit.com/r/TubeArchivist/">Reddit</a>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
@ -1,20 +0,0 @@
|
|||||||
import dynamic from "next/dynamic";
|
|
||||||
|
|
||||||
const Header = ({ authData }) => {
|
|
||||||
const { session, status } = authData;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<h1>Name: {session?.user?.name}</h1>
|
|
||||||
<h1>Status: {status}</h1>
|
|
||||||
<h1>Token: {session?.ta_token?.token}</h1>
|
|
||||||
<h1>User ID: {session?.ta_token?.user_id}</h1>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const DynamicHeader = dynamic(() => import("../components/Header"), {
|
|
||||||
suspense: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
export default Header;
|
|
@ -1,14 +0,0 @@
|
|||||||
import { Footer } from "./Footer";
|
|
||||||
import { Nav } from "./Nav";
|
|
||||||
|
|
||||||
export const Layout = ({ children }) => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div style={{ minHeight: "100vh" }} className="main-content">
|
|
||||||
<Nav />
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
<Footer />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,95 +0,0 @@
|
|||||||
import NextImage from "next/image";
|
|
||||||
import NextLink from "next/link";
|
|
||||||
import BannerDark from "../images/banner-tube-archivist-dark.png";
|
|
||||||
import IconSearch from "../images/icon-search.svg";
|
|
||||||
import IconGear from "../images/icon-gear.svg";
|
|
||||||
import IconExit from "../images/icon-exit.svg";
|
|
||||||
import { signIn, signOut, useSession } from "next-auth/react";
|
|
||||||
|
|
||||||
/** TODO: Fix these nav links */
|
|
||||||
export const Nav = () => {
|
|
||||||
const { data: session } = useSession();
|
|
||||||
|
|
||||||
const handleSigninSignout = () => {
|
|
||||||
if (!session) {
|
|
||||||
signIn();
|
|
||||||
}
|
|
||||||
signOut();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="boxed-content">
|
|
||||||
<div className="top-banner">
|
|
||||||
<NextLink href="/">
|
|
||||||
<a>
|
|
||||||
{/* {% if colors == 'dark */}
|
|
||||||
<NextImage
|
|
||||||
width={700}
|
|
||||||
height={150}
|
|
||||||
src={BannerDark}
|
|
||||||
alt="tube-archivist-banner"
|
|
||||||
/>
|
|
||||||
{/* {% endif %} */}
|
|
||||||
{/* {% if colors == 'light */}
|
|
||||||
{/* <img src="/img/banner-tube-archivist-light.png" alt="tube-archivist-banner"> */}
|
|
||||||
{/* {% endif %} */}
|
|
||||||
</a>
|
|
||||||
</NextLink>
|
|
||||||
</div>
|
|
||||||
<div className="top-nav">
|
|
||||||
<div className="nav-items">
|
|
||||||
<NextLink href="/">
|
|
||||||
<a>
|
|
||||||
<div className="nav-item">home</div>
|
|
||||||
</a>
|
|
||||||
</NextLink>
|
|
||||||
<NextLink href="/channel">
|
|
||||||
<a>
|
|
||||||
<div className="nav-item">channels</div>
|
|
||||||
</a>
|
|
||||||
</NextLink>
|
|
||||||
<NextLink href="/playlist">
|
|
||||||
<a>
|
|
||||||
<div className="nav-item">playlists</div>
|
|
||||||
</a>
|
|
||||||
</NextLink>
|
|
||||||
<NextLink href="/download">
|
|
||||||
<a>
|
|
||||||
<div className="nav-item">downloads</div>
|
|
||||||
</a>
|
|
||||||
</NextLink>
|
|
||||||
</div>
|
|
||||||
<div className="nav-icons">
|
|
||||||
<a href="/search">
|
|
||||||
<NextImage
|
|
||||||
width={50}
|
|
||||||
height={40}
|
|
||||||
src={IconSearch}
|
|
||||||
alt="search-icon"
|
|
||||||
title="Search"
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
<a href="/settings">
|
|
||||||
<NextImage
|
|
||||||
width={50}
|
|
||||||
height={40}
|
|
||||||
src={IconGear}
|
|
||||||
alt="gear-icon"
|
|
||||||
title="Settings"
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
<a style={{ cursor: "pointer" }} onClick={handleSigninSignout}>
|
|
||||||
<NextImage
|
|
||||||
width={50}
|
|
||||||
height={40}
|
|
||||||
className="alert-hover"
|
|
||||||
src={IconExit}
|
|
||||||
alt="exit-icon"
|
|
||||||
title="Logout"
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,223 +0,0 @@
|
|||||||
import { useSession } from "next-auth/react";
|
|
||||||
import NextImage from "next/image";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { useQuery } from "react-query";
|
|
||||||
import IconPlay from "../../images/icon-play.svg";
|
|
||||||
import { getTAUrl } from "../../lib/constants";
|
|
||||||
import { getVideos } from "../../lib/getVideos";
|
|
||||||
import type { Data } from "../../types/video";
|
|
||||||
import VideoPlayer from "../VideoPlayer";
|
|
||||||
|
|
||||||
type ViewStyle = "grid" | "list";
|
|
||||||
|
|
||||||
const TA_BASE_URL = getTAUrl();
|
|
||||||
|
|
||||||
const VideoList = () => {
|
|
||||||
const [selectedVideoUrl, setSelectedVideoUrl] = useState<Data>();
|
|
||||||
const [viewStyle, setViewStyle] = useState<ViewStyle>("grid");
|
|
||||||
const { data: session } = useSession();
|
|
||||||
const { data, error, isLoading } = useQuery(
|
|
||||||
["videos", session.ta_token.token],
|
|
||||||
() => getVideos(session.ta_token.token),
|
|
||||||
{
|
|
||||||
enabled: !!session?.ta_token?.token,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleSelectedVideo = (video: Data) => {
|
|
||||||
setSelectedVideoUrl(video);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRemoveVideoPlayer = () => {
|
|
||||||
setSelectedVideoUrl(undefined);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSetViewstyle = (selectedViewStyle: ViewStyle) => {
|
|
||||||
setViewStyle(selectedViewStyle);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!isLoading && !data?.data) {
|
|
||||||
return (
|
|
||||||
<div className="boxed-content">
|
|
||||||
<h2>No videos found...</h2>
|
|
||||||
<p>
|
|
||||||
If you've already added a channel or playlist, try going to the{" "}
|
|
||||||
<a href="{% url 'downloads">downloads page</a> to start the scan and
|
|
||||||
download tasks.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<VideoPlayer
|
|
||||||
handleRemoveVideoPlayer={handleRemoveVideoPlayer}
|
|
||||||
selectedVideo={selectedVideoUrl}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="boxed-content">
|
|
||||||
<div className="title-bar">
|
|
||||||
<h1>Recent Videos</h1>
|
|
||||||
</div>
|
|
||||||
<div className="view-controls">
|
|
||||||
<div className="toggle">
|
|
||||||
<span>Hide watched:</span>
|
|
||||||
<div className="toggleBox">
|
|
||||||
<input
|
|
||||||
id="hide_watched"
|
|
||||||
// onClick="toggleCheckbox(this)"
|
|
||||||
type="checkbox"
|
|
||||||
/>
|
|
||||||
{/* {% if not hide_watched %} */}
|
|
||||||
<label htmlFor="" className="ofbtn">
|
|
||||||
Off
|
|
||||||
</label>
|
|
||||||
{/* {% else %} */}
|
|
||||||
<label htmlFor="" className="onbtn">
|
|
||||||
On
|
|
||||||
</label>
|
|
||||||
{/* {% endif %} */}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="sort">
|
|
||||||
<div id="hidden-form">
|
|
||||||
<span>Sort by:</span>
|
|
||||||
<select
|
|
||||||
name="sort"
|
|
||||||
id="sort"
|
|
||||||
onChange={() => console.log("onChange sort")}
|
|
||||||
>
|
|
||||||
<option value="published">date published</option>
|
|
||||||
<option value="downloaded">date downloaded</option>
|
|
||||||
<option value="views">views</option>
|
|
||||||
<option value="likes">likes</option>
|
|
||||||
</select>
|
|
||||||
<select
|
|
||||||
name="sord-order"
|
|
||||||
id="sort-order"
|
|
||||||
onChange={() => console.log("onChange sort-order")}
|
|
||||||
>
|
|
||||||
<option value="asc">asc</option>
|
|
||||||
<option value="desc">desc</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="view-icons">
|
|
||||||
<img
|
|
||||||
src="/img/icon-sort.svg"
|
|
||||||
alt="sort-icon"
|
|
||||||
onClick={() => console.log("showForm")}
|
|
||||||
id="animate-icon"
|
|
||||||
/>
|
|
||||||
<img
|
|
||||||
src="/img/icon-gridview.svg"
|
|
||||||
onClick={() => handleSetViewstyle("grid")}
|
|
||||||
alt="grid view"
|
|
||||||
/>
|
|
||||||
<img
|
|
||||||
src="/img/icon-listview.svg"
|
|
||||||
onClick={() => handleSetViewstyle("list")}
|
|
||||||
alt="list view"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className={`video-list ${viewStyle}`}>
|
|
||||||
{data &&
|
|
||||||
data?.data?.map((video) => {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={video.youtube_id}
|
|
||||||
className={`video-item ${viewStyle}`}
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
style={{ cursor: "pointer" }}
|
|
||||||
onClick={() => handleSelectedVideo(video)}
|
|
||||||
>
|
|
||||||
<div className="video-thumb-wrap list">
|
|
||||||
<div className="video-thumb">
|
|
||||||
<NextImage
|
|
||||||
src={`${TA_BASE_URL.client}${video.vid_thumb_url}`}
|
|
||||||
alt="video-thumb"
|
|
||||||
width={640}
|
|
||||||
height={360}
|
|
||||||
// blurDataURL={video.vid_thumb_base64}
|
|
||||||
// placeholder="blur"
|
|
||||||
/>
|
|
||||||
{/* {% if video.source.player.progress %} */}
|
|
||||||
<div
|
|
||||||
className="video-progress-bar"
|
|
||||||
id={`progress-${video.youtube_id}`}
|
|
||||||
// style={{ width: video.player.progress }} // TODO: /video/youtube_id/progress
|
|
||||||
></div>
|
|
||||||
{/* {% else %} */}
|
|
||||||
<div
|
|
||||||
className="video-progress-bar"
|
|
||||||
id={`progress-${video.youtube_id}`}
|
|
||||||
style={{ width: "0%" }}
|
|
||||||
></div>
|
|
||||||
{/* {% endif %} */}
|
|
||||||
</div>
|
|
||||||
<div className="video-play">
|
|
||||||
<NextImage
|
|
||||||
width={40}
|
|
||||||
height={40}
|
|
||||||
src={IconPlay}
|
|
||||||
alt="play-icon"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
<div className="video-desc list">
|
|
||||||
<div
|
|
||||||
className="video-desc-player"
|
|
||||||
id={`video-info-${video.youtube_id}`}
|
|
||||||
>
|
|
||||||
{video?.player?.watched ? (
|
|
||||||
<img
|
|
||||||
src="/img/icon-seen.svg"
|
|
||||||
alt="seen-icon"
|
|
||||||
data-id={video.youtube_id}
|
|
||||||
data-status="watched"
|
|
||||||
// onClick="updateVideoWatchStatus(this)"
|
|
||||||
className="watch-button"
|
|
||||||
title="Mark as unwatched"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<img
|
|
||||||
src="/img/icon-unseen.svg"
|
|
||||||
alt="unseen-icon"
|
|
||||||
data-id={video.youtube_id}
|
|
||||||
data-status="unwatched"
|
|
||||||
// onClick="updateVideoWatchStatus(this)"
|
|
||||||
className="watch-button"
|
|
||||||
title="Mark as watched"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<span>
|
|
||||||
{video.published} | {video.player.duration_str}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<a href={`/channel/${video.channel.channel_id}`}>
|
|
||||||
<h3>{video.channel.channel_name}</h3>
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
className="video-more"
|
|
||||||
href={`/video/${video.youtube_id}`}
|
|
||||||
>
|
|
||||||
<h2>{video.title}</h2>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default VideoList;
|
|
@ -1,4 +0,0 @@
|
|||||||
import dynamic from "next/dynamic";
|
|
||||||
|
|
||||||
const DynamicVideoList = dynamic(() => import("./VideoList"));
|
|
||||||
export default DynamicVideoList;
|
|
@ -1,126 +0,0 @@
|
|||||||
import NextImage from "next/image";
|
|
||||||
import NextLink from "next/link";
|
|
||||||
import ReactPlayer from "react-player";
|
|
||||||
import IconClose from "../../images/icon-close.svg";
|
|
||||||
import { getTAUrl } from "../../lib/constants";
|
|
||||||
import { formatNumbers } from "../../lib/utils";
|
|
||||||
import { Data } from "../../types/video";
|
|
||||||
|
|
||||||
const TA_BASE_URL = getTAUrl();
|
|
||||||
|
|
||||||
type VideoPlayerProps = {
|
|
||||||
selectedVideo: Data;
|
|
||||||
handleRemoveVideoPlayer?: () => void;
|
|
||||||
isHome?: boolean;
|
|
||||||
showStats?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
const VideoPlayer = ({
|
|
||||||
selectedVideo,
|
|
||||||
handleRemoveVideoPlayer,
|
|
||||||
isHome = true,
|
|
||||||
showStats = true,
|
|
||||||
}: VideoPlayerProps) => {
|
|
||||||
if (!selectedVideo) return;
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{selectedVideo && (
|
|
||||||
<div className="player-wrapper">
|
|
||||||
<div className="video-player">
|
|
||||||
<ReactPlayer
|
|
||||||
controls={true}
|
|
||||||
width="100%"
|
|
||||||
height="100%"
|
|
||||||
light={false}
|
|
||||||
playing // TODO: Not currently working
|
|
||||||
playsinline
|
|
||||||
url={`${TA_BASE_URL.client}${selectedVideo?.media_url}`}
|
|
||||||
/>
|
|
||||||
<SponsorBlock />
|
|
||||||
{showStats ? (
|
|
||||||
<div className="player-title boxed-content">
|
|
||||||
<NextImage
|
|
||||||
className="close-button"
|
|
||||||
src={IconClose}
|
|
||||||
width={30}
|
|
||||||
height={30}
|
|
||||||
alt="close-icon"
|
|
||||||
onClick={handleRemoveVideoPlayer}
|
|
||||||
title="Close player"
|
|
||||||
/>
|
|
||||||
<div className="thumb-icon player-stats">
|
|
||||||
<img src="/img/icon-eye.svg" alt="views icon" />
|
|
||||||
<span>
|
|
||||||
{formatNumbers(selectedVideo.stats.view_count.toString())}
|
|
||||||
</span>
|
|
||||||
<span>|</span>
|
|
||||||
<img src="/img/icon-thumb.svg" alt="thumbs-up" />
|
|
||||||
<span>
|
|
||||||
{formatNumbers(selectedVideo.stats.like_count.toString())}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="player-channel-playlist">
|
|
||||||
<h3>
|
|
||||||
<a href="/channel/${channelId}/">
|
|
||||||
{selectedVideo.channel.channel_name}
|
|
||||||
</a>
|
|
||||||
</h3>
|
|
||||||
{/* ${playlist} */}
|
|
||||||
</div>
|
|
||||||
<NextLink href={`/video/${selectedVideo.youtube_id}/`}>
|
|
||||||
<a>
|
|
||||||
<h2 id="video-title">{selectedVideo.title}</h2>
|
|
||||||
</a>
|
|
||||||
</NextLink>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default VideoPlayer;
|
|
||||||
|
|
||||||
function SponsorBlock() {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{/* <div className="notifications" id="notifications"></div> */}
|
|
||||||
<div className="sponsorblock" id="sponsorblock">
|
|
||||||
{/* {% if video.sponsorblock.is_enabled %} */}
|
|
||||||
{/* {% if video.sponsorblock.segments|length == 0 %} */}
|
|
||||||
<h4>
|
|
||||||
This video doesn't have any sponsor segments added. To add a
|
|
||||||
segment go to{" "}
|
|
||||||
<u>
|
|
||||||
<a href="https://www.youtube.com/watch?v={{ video.youtube_id }}">
|
|
||||||
this video on YouTube
|
|
||||||
</a>
|
|
||||||
</u>{" "}
|
|
||||||
and add a segment using the{" "}
|
|
||||||
<u>
|
|
||||||
<a href="https://sponsor.ajay.app/">SponsorBlock</a>
|
|
||||||
</u>{" "}
|
|
||||||
extension.
|
|
||||||
</h4>
|
|
||||||
{/* {% elif video.sponsorblock.has_unlocked %} */}
|
|
||||||
<h4>
|
|
||||||
This video has unlocked sponsor segments. Go to{" "}
|
|
||||||
<u>
|
|
||||||
<a href="https://www.youtube.com/watch?v={{ video.youtube_id }}">
|
|
||||||
this video on YouTube
|
|
||||||
</a>
|
|
||||||
</u>{" "}
|
|
||||||
and vote on the segments using the{" "}
|
|
||||||
<u>
|
|
||||||
<a href="https://sponsor.ajay.app/">SponsorBlock</a>
|
|
||||||
</u>{" "}
|
|
||||||
extension.
|
|
||||||
</h4>
|
|
||||||
{/* {% endif %} */}
|
|
||||||
{/* {% endif %} */}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,4 +0,0 @@
|
|||||||
import dynamic from "next/dynamic";
|
|
||||||
|
|
||||||
const DynamicVideoPlayer = dynamic(() => import("./VideoPlayer"));
|
|
||||||
export default DynamicVideoPlayer;
|
|
Before Width: | Height: | Size: 51 KiB |
Before Width: | Height: | Size: 54 KiB |
Before Width: | Height: | Size: 61 KiB |
Before Width: | Height: | Size: 84 KiB |
Before Width: | Height: | Size: 56 KiB |
@ -1,71 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
|
||||||
|
|
||||||
<svg
|
|
||||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
|
||||||
xmlns:cc="http://creativecommons.org/ns#"
|
|
||||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
||||||
width="500"
|
|
||||||
height="500"
|
|
||||||
viewBox="0 0 132.29197 132.29167"
|
|
||||||
version="1.1"
|
|
||||||
id="svg1303"
|
|
||||||
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
|
|
||||||
sodipodi:docname="Icon_add.svg">
|
|
||||||
<defs
|
|
||||||
id="defs1297" />
|
|
||||||
<sodipodi:namedview
|
|
||||||
id="base"
|
|
||||||
pagecolor="#ffffff"
|
|
||||||
bordercolor="#666666"
|
|
||||||
borderopacity="1.0"
|
|
||||||
inkscape:pageopacity="0.0"
|
|
||||||
inkscape:pageshadow="2"
|
|
||||||
inkscape:zoom="0.85859018"
|
|
||||||
inkscape:cx="-97.380081"
|
|
||||||
inkscape:cy="261.09215"
|
|
||||||
inkscape:document-units="mm"
|
|
||||||
inkscape:current-layer="layer1"
|
|
||||||
showgrid="false"
|
|
||||||
units="px"
|
|
||||||
inkscape:window-width="1920"
|
|
||||||
inkscape:window-height="1017"
|
|
||||||
inkscape:window-x="-8"
|
|
||||||
inkscape:window-y="-8"
|
|
||||||
inkscape:window-maximized="1"
|
|
||||||
showguides="true"
|
|
||||||
inkscape:guide-bbox="true">
|
|
||||||
<sodipodi:guide
|
|
||||||
position="-221.87586,143.2945"
|
|
||||||
orientation="1,0"
|
|
||||||
id="guide1072"
|
|
||||||
inkscape:locked="false" />
|
|
||||||
</sodipodi:namedview>
|
|
||||||
<metadata
|
|
||||||
id="metadata1300">
|
|
||||||
<rdf:RDF>
|
|
||||||
<cc:Work
|
|
||||||
rdf:about="">
|
|
||||||
<dc:format>image/svg+xml</dc:format>
|
|
||||||
<dc:type
|
|
||||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
|
||||||
<dc:title></dc:title>
|
|
||||||
</cc:Work>
|
|
||||||
</rdf:RDF>
|
|
||||||
</metadata>
|
|
||||||
<g
|
|
||||||
inkscape:label="Ebene 1"
|
|
||||||
inkscape:groupmode="layer"
|
|
||||||
id="layer1"
|
|
||||||
transform="translate(0,-164.70764)">
|
|
||||||
<path
|
|
||||||
style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0;stroke-linecap:round;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:markers fill stroke"
|
|
||||||
d="m 58.600542,170.62113 c -1.62283,1.61686 -2.626579,3.8573 -2.631447,6.33943 l -0.08037,43.5977 -43.597706,-0.0803 c -4.9648228,-0.009 -8.9691711,3.98046 -8.9784054,8.94536 l -0.00482,2.62846 c -0.00925,4.9649 3.9804459,8.96933 8.9452674,8.97832 l 43.597694,0.0805 -0.08027,43.59778 c -0.0093,4.96488 3.980368,8.96922 8.945263,8.9783 l 2.628471,0.005 c 4.964897,0.009 8.969245,-3.98054 8.978406,-8.94536 l 0.08035,-43.59771 43.597715,0.0803 c 4.96484,0.009 8.96917,-3.98046 8.9784,-8.94534 l 0.005,-2.62847 c 0.009,-4.96489 -3.98037,-8.96923 -8.94525,-8.97831 l -43.597784,-0.0805 0.08034,-43.59771 c 0.0093,-4.96481 -3.980379,-8.96923 -8.945267,-8.97831 l -2.628469,-0.005 c -2.482483,-0.005 -4.724106,0.98906 -6.346936,2.60592 z"
|
|
||||||
id="rect1073"
|
|
||||||
inkscape:connector-curvature="0" />
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 2.9 KiB |
@ -1,63 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
|
||||||
|
|
||||||
<svg
|
|
||||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
|
||||||
xmlns:cc="http://creativecommons.org/ns#"
|
|
||||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
||||||
width="500"
|
|
||||||
height="500"
|
|
||||||
viewBox="0 0 132.29197 132.29167"
|
|
||||||
version="1.1"
|
|
||||||
id="svg1303"
|
|
||||||
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
|
|
||||||
sodipodi:docname="Icons_close.svg">
|
|
||||||
<defs
|
|
||||||
id="defs1297" />
|
|
||||||
<sodipodi:namedview
|
|
||||||
id="base"
|
|
||||||
pagecolor="#ffffff"
|
|
||||||
bordercolor="#666666"
|
|
||||||
borderopacity="1.0"
|
|
||||||
inkscape:pageopacity="0.0"
|
|
||||||
inkscape:pageshadow="2"
|
|
||||||
inkscape:zoom="1.4291625"
|
|
||||||
inkscape:cx="122.66624"
|
|
||||||
inkscape:cy="202.38142"
|
|
||||||
inkscape:document-units="mm"
|
|
||||||
inkscape:current-layer="layer1"
|
|
||||||
showgrid="false"
|
|
||||||
units="px"
|
|
||||||
inkscape:window-width="1920"
|
|
||||||
inkscape:window-height="1017"
|
|
||||||
inkscape:window-x="-8"
|
|
||||||
inkscape:window-y="-8"
|
|
||||||
inkscape:window-maximized="1" />
|
|
||||||
<metadata
|
|
||||||
id="metadata1300">
|
|
||||||
<rdf:RDF>
|
|
||||||
<cc:Work
|
|
||||||
rdf:about="">
|
|
||||||
<dc:format>image/svg+xml</dc:format>
|
|
||||||
<dc:type
|
|
||||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
|
||||||
<dc:title />
|
|
||||||
</cc:Work>
|
|
||||||
</rdf:RDF>
|
|
||||||
</metadata>
|
|
||||||
<g
|
|
||||||
inkscape:label="Ebene 1"
|
|
||||||
inkscape:groupmode="layer"
|
|
||||||
id="layer1"
|
|
||||||
transform="translate(0,-164.70764)">
|
|
||||||
<path
|
|
||||||
style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0;stroke-linecap:round;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:markers fill stroke"
|
|
||||||
d="m 115.5244,167.09017 c -3.04348,0 -6.08905,1.16742 -8.42111,3.49893 L 66.146003,211.54641 25.18874,170.5891 c -4.664143,-4.66413 -12.173961,-4.66413 -16.8382429,0 l -2.4692132,2.46935 c -4.664282,4.66413 -4.664282,12.17408 0,16.83807 l 40.9572491,40.9573 -40.9572491,40.95745 c -4.664282,4.66412 -4.664282,12.17393 0,16.83806 l 2.4692132,2.46935 c 4.6642819,4.66413 12.1740999,4.66413 16.8382429,0 l 40.957263,-40.9573 40.957287,40.9573 c 4.66413,4.66413 12.17393,4.66413 16.8382,0 l 2.46921,-2.46935 c 4.66427,-4.66413 4.66427,-12.17394 0,-16.83806 L 85.453463,230.85382 126.4107,189.89652 c 4.66427,-4.66399 4.66427,-12.17394 0,-16.83807 l -2.46921,-2.46935 c -2.33221,-2.33206 -5.37361,-3.49893 -8.41709,-3.49893 z"
|
|
||||||
id="rect1073"
|
|
||||||
inkscape:connector-curvature="0" />
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 2.6 KiB |