chore: remove tubearchivist backend files
22
.github/workflows/lint_python.yml
vendored
@ -1,22 +0,0 @@
|
|||||||
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: black --check --diff --line-length 79 .
|
|
||||||
- run: codespell
|
|
||||||
- run: flake8 . --count --max-complexity=10 --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
|
|
39
.gitignore
vendored
@ -1,8 +1,35 @@
|
|||||||
# python testing cache
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
__pycache__
|
|
||||||
|
|
||||||
# django testing db
|
# dependencies
|
||||||
db.sqlite3
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
# vscode custom conf
|
# testing
|
||||||
.vscode
|
/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
|
||||||
|
218
README.md
@ -1,202 +1,34 @@
|
|||||||
![Tube Archivist](assets/tube-archivist-banner.jpg?raw=true "Tube Archivist Banner")
|
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).
|
||||||
|
|
||||||
<center><h1>Your self hosted YouTube media server</h1></center>
|
|
||||||
|
|
||||||
Tube Archivist has a new home: https://github.com/tubearchivist/tubearchivist
|
|
||||||
|
|
||||||
## Table of contents:
|
|
||||||
* [Wiki](https://github.com/tubearchivist/tubearchivist/wiki) for a detailed documentation, with [FAQ](https://github.com/tubearchivist/tubearchivist/wiki/FAQ)
|
|
||||||
* [Core functionality](#core-functionality)
|
|
||||||
* [Screenshots](#screenshots)
|
|
||||||
* [Problem Tube Archivist tries to solve](#problem-tube-archivist-tries-to-solve)
|
|
||||||
* [Connect](#connect)
|
|
||||||
* [Installing and updating](#installing-and-updating)
|
|
||||||
* [Getting Started](#getting-started)
|
|
||||||
* [Potential pitfalls](#potential-pitfalls)
|
|
||||||
* [Roadmap](#roadmap)
|
|
||||||
* [Known limitations](#known-limitations)
|
|
||||||
* [Donate](#donate)
|
|
||||||
|
|
||||||
------------------------
|
|
||||||
|
|
||||||
## 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
|
|
||||||
|
|
||||||
## Tube Archivist on YouTube
|
|
||||||
[![ibracorp-youtube-video-thumb](assets/tube-archivist-ibracorp-O8H8Z01c0Ys.jpg)](https://www.youtube.com/watch?v=O8H8Z01c0Ys)
|
|
||||||
|
|
||||||
## 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.
|
|
||||||
|
|
||||||
## Connect
|
|
||||||
- [Discord](https://discord.gg/AFwz8nE7BK): Connect with us on our Discord server.
|
|
||||||
- [r/TubeArchivist](https://www.reddit.com/r/TubeArchivist/): Join our Subreddit.
|
|
||||||
|
|
||||||
## Installing and updating
|
|
||||||
Take a look at the example `docker-compose.yml` file provided. Use the *latest* or the named semantic version tag. The *unstable* tag is for intermediate testing and as the name implies, is **unstable** and not be used on your main installation but in a [testing environment](CONTRIBUTING.md).
|
|
||||||
|
|
||||||
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*.
|
|
||||||
- For the scheduler to know what time it is, set your timezone with the `TZ` environment variable, defaults to *UTC*.
|
|
||||||
|
|
||||||
### Port collisions
|
|
||||||
If you have a collision on port `8000`, best solution is to use dockers *HOST_PORT* and *CONTAINER_PORT* distinction: To for example change the interface to port 9000 use `9000:8000` in your docker-compose file.
|
|
||||||
|
|
||||||
Should that not be an option, the Tube Archivist container takes these two additional environment variables:
|
|
||||||
- **TA_PORT**: To actually change the port where nginx listens, make sure to also change the ports value in your docker-compose file.
|
|
||||||
- **TA_UWSGI_PORT**: To change the default uwsgi port 8080 used for container internal networking between uwsgi serving the django application and nginx.
|
|
||||||
|
|
||||||
Changing any of these two environment variables will change the files *nginx.conf* and *uwsgi.ini* at startup using `sed` in your container.
|
|
||||||
|
|
||||||
### Elasticsearch
|
|
||||||
**Note**: Newest Tube Archivist depends on Elasticsearch version 7.17 to provide an automatic updatepath in the future.
|
|
||||||
|
|
||||||
Use `bbilly1/tubearchivist-es` to automatically get the recommended version, or use the official image with the version tag in the docker-compose file.
|
|
||||||
|
|
||||||
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. Use `bbilly1/tubearchivist-es` to automatically get the recommended version.
|
|
||||||
|
|
||||||
### Alternative installation instructions:
|
|
||||||
- **arm64**: The 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.
|
|
||||||
- **Synology**: There is a [discussion thread](https://github.com/tubearchivist/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").
|
|
||||||
- **Helm Chart**: There is a Helm Chart available at https://github.com/insuusvenerati/helm-charts. Mostly self-explanatory but feel free to ask questions in the discord / subreddit.
|
|
||||||
|
|
||||||
|
|
||||||
## 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.
|
|
||||||
|
|
||||||
### Disk usage
|
|
||||||
The Elasticsearch index will turn to *read only* if the disk usage of the container goes above 95% until the usage drops below 90% again, you will see error messages like `disk usage exceeded flood-stage watermark`, [link](https://github.com/tubearchivist/tubearchivist#disk-usage).
|
|
||||||
|
|
||||||
Similar to that, TubeArchivist will become all sorts of messed up when running out of disk space. There are some error messages in the logs when that happens, but it's best to make sure to have enough disk space before starting to download.
|
|
||||||
|
|
||||||
## 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 and some other browsers 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
|
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||||
We have come far, nonetheless we are not short of ideas on how to improve and extend this project. Issues waiting for you to be tackled in no particular order:
|
|
||||||
|
|
||||||
- [ ] User roles
|
You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file.
|
||||||
- [ ] Podcast mode to serve channel as mp3
|
|
||||||
- [ ] Implement [PyFilesystem](https://github.com/PyFilesystem/pyfilesystem2) for flexible video storage
|
|
||||||
- [ ] Implement [Apprise](https://github.com/caronc/apprise) for notifications ([#97](https://github.com/tubearchivist/tubearchivist/issues/97))
|
|
||||||
- [ ] Add passing browser cookies to yt-dlp ([#199](https://github.com/tubearchivist/tubearchivist/issues/199))
|
|
||||||
- [ ] User created playlists, random and repeat controls ([#108](https://github.com/tubearchivist/tubearchivist/issues/108), [#220](https://github.com/tubearchivist/tubearchivist/issues/220))
|
|
||||||
- [ ] Auto play or play next link ([#226](https://github.com/tubearchivist/tubearchivist/issues/226))
|
|
||||||
- [ ] Show similar videos on video page
|
|
||||||
- [ ] Multi language support
|
|
||||||
- [ ] Show total video downloaded vs total videos available in channel
|
|
||||||
- [ ] Make items in grid row configurable to use more of the screen
|
|
||||||
- [ ] Add statistics of index
|
|
||||||
- [ ] Implement complete offline media file import from json file ([#138](https://github.com/tubearchivist/tubearchivist/issues/138))
|
|
||||||
- [ ] Filter and query in search form, search by url query ([#134](https://github.com/tubearchivist/tubearchivist/issues/134), [#139](https://github.com/tubearchivist/tubearchivist/issues/139))
|
|
||||||
- [ ] Auto ignore videos by keyword ([#163](https://github.com/tubearchivist/tubearchivist/issues/163))
|
|
||||||
- [ ] Custom searchable notes to videos, channels, playlists ([#144](https://github.com/tubearchivist/tubearchivist/issues/144))
|
|
||||||
- [ ] Download video comments
|
|
||||||
|
|
||||||
Implemented:
|
[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`.
|
||||||
- [X] Add [SponsorBlock](https://sponsor.ajay.app/) integration [2022-04-16]
|
|
||||||
- [X] Implement per channel settings [2022-03-26]
|
|
||||||
- [X] Subtitle download & indexing [2022-02-13]
|
|
||||||
- [X] Fancy advanced unified search interface [2022-01-08]
|
|
||||||
- [X] Auto rescan and auto download on a schedule [2021-12-17]
|
|
||||||
- [X] Optional automatic deletion of watched items after a specified time [2021-12-17]
|
|
||||||
- [X] Create playlists [2021-11-27]
|
|
||||||
- [X] Access control [2021-11-01]
|
|
||||||
- [X] Delete videos and channel [2021-10-16]
|
|
||||||
- [X] Add thumbnail embed option [2021-10-16]
|
|
||||||
- [X] Create a github wiki for user documentation [2021-10-03]
|
|
||||||
- [X] Grid and list view for both channel and video list pages [2021-10-03]
|
|
||||||
- [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]
|
|
||||||
|
|
||||||
## Known limitations
|
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.
|
||||||
- Video files created by Tube Archivist need to be playable in your browser of choice. Not every codec is compatible with every browser and might require some testing with format selection.
|
|
||||||
- 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.
|
|
||||||
|
|
||||||
|
## Learn More
|
||||||
|
|
||||||
## Donate
|
To learn more about Next.js, take a look at the following resources:
|
||||||
The best donation to **Tube Archivist** is your time, take a look at the [contribution page](CONTRIBUTING.md) to get started.
|
|
||||||
Second best way to support the development is to provide for caffeinated beverages:
|
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||||
* [Paypal.me](https://paypal.me/bbilly1) for a one time coffee
|
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||||
* [Paypal Subscription](https://www.paypal.com/webapps/billing/plans/subscribe?plan_id=P-03770005GR991451KMFGVPMQ) for a monthly coffee
|
|
||||||
* [ko-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.
|
||||||
|
205
deploy.sh
@ -1,205 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# deploy all needed project files to different servers:
|
|
||||||
# test for local vm for testing
|
|
||||||
# blackhole for local production
|
|
||||||
# unstable to publish intermediate releases
|
|
||||||
# docker to publish regular release
|
|
||||||
|
|
||||||
# create builder:
|
|
||||||
# docker buildx create --name tubearchivist
|
|
||||||
# docker buildx use tubearchivist
|
|
||||||
# docker buildx inspect --bootstrap
|
|
||||||
|
|
||||||
# more details:
|
|
||||||
# https://github.com/tubearchivist/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"
|
|
||||||
# make base folder
|
|
||||||
ssh "$host" "mkdir -p docker"
|
|
||||||
|
|
||||||
# copy project files to build image
|
|
||||||
rsync -a --progress --delete-after \
|
|
||||||
--exclude ".git" \
|
|
||||||
--exclude ".gitignore" \
|
|
||||||
--exclude "**/cache" \
|
|
||||||
--exclude "**/__pycache__/" \
|
|
||||||
--exclude "db.sqlite3" \
|
|
||||||
. -e ssh "$host":tubearchivist
|
|
||||||
|
|
||||||
# copy default docker-compose file if not exist
|
|
||||||
rsync --progress --ignore-existing 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 --build-arg INSTALL_DEBUG=1 --platform $platform -t bbilly1/tubearchivist:latest tubearchivist --load"
|
|
||||||
ssh "$host" 'docker-compose -f docker/docker-compose.yml up -d'
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# 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 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=10 --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"
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# update latest tag compatible es for set and forget
|
|
||||||
function sync_latest_es {
|
|
||||||
|
|
||||||
VERSION=$(grep "bbilly1/tubearchivist-es" docker-compose.yml | awk '{print $NF}')
|
|
||||||
printf "\nsync new ES version %s\nContinue?\n" "$VERSION"
|
|
||||||
read -rn 1
|
|
||||||
|
|
||||||
if [[ $(systemctl is-active docker) != 'active' ]]; then
|
|
||||||
echo "starting docker"
|
|
||||||
sudo systemctl start docker
|
|
||||||
fi
|
|
||||||
|
|
||||||
sudo docker image pull docker.elastic.co/elasticsearch/elasticsearch:"$VERSION"
|
|
||||||
|
|
||||||
sudo docker tag \
|
|
||||||
docker.elastic.co/elasticsearch/elasticsearch:"$VERSION" \
|
|
||||||
bbilly1/tubearchivist-es
|
|
||||||
|
|
||||||
sudo docker tag \
|
|
||||||
docker.elastic.co/elasticsearch/elasticsearch:"$VERSION" \
|
|
||||||
bbilly1/tubearchivist-es:"$VERSION"
|
|
||||||
|
|
||||||
sudo docker push bbilly1/tubearchivist-es
|
|
||||||
sudo docker push bbilly1/tubearchivist-es:"$VERSION"
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# publish unstable tag to docker
|
|
||||||
function sync_unstable {
|
|
||||||
|
|
||||||
if [[ $(systemctl is-active docker) != 'active' ]]; then
|
|
||||||
echo "starting docker"
|
|
||||||
sudo systemctl start docker
|
|
||||||
fi
|
|
||||||
|
|
||||||
# start amd64 build
|
|
||||||
sudo docker buildx build \
|
|
||||||
--platform linux/amd64 \
|
|
||||||
-t bbilly1/tubearchivist:unstable --push .
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
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 | tail -n 10
|
|
||||||
|
|
||||||
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"
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if [[ $1 == "blackhole" ]]; then
|
|
||||||
sync_blackhole
|
|
||||||
elif [[ $1 == "test" ]]; then
|
|
||||||
sync_test "$2"
|
|
||||||
elif [[ $1 == "validate" ]]; then
|
|
||||||
# check package versions in requirements.txt for updates
|
|
||||||
python version_check.py
|
|
||||||
validate "$2"
|
|
||||||
elif [[ $1 == "docker" ]]; then
|
|
||||||
sync_docker
|
|
||||||
sync_unstable
|
|
||||||
elif [[ $1 == "unstable" ]]; then
|
|
||||||
sync_unstable
|
|
||||||
elif [[ $1 == "es" ]]; then
|
|
||||||
sync_latest_es
|
|
||||||
else
|
|
||||||
echo "valid options are: blackhole | test | validate | docker | unstable | es"
|
|
||||||
fi
|
|
||||||
|
|
||||||
|
|
||||||
##
|
|
||||||
exit 0
|
|
@ -1,29 +0,0 @@
|
|||||||
server {
|
|
||||||
|
|
||||||
listen 8000;
|
|
||||||
|
|
||||||
location /cache/videos/ {
|
|
||||||
alias /cache/videos/;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /cache/channels/ {
|
|
||||||
alias /cache/channels/;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /cache/playlists/ {
|
|
||||||
alias /cache/playlists/;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /media/ {
|
|
||||||
alias /youtube/;
|
|
||||||
types {
|
|
||||||
text/vtt vtt;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
location / {
|
|
||||||
include uwsgi_params;
|
|
||||||
uwsgi_pass localhost:8080;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,50 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# startup script inside the container for tubearchivist
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
# ugly nginx and uwsgi port overwrite with env vars
|
|
||||||
if [[ -n "$TA_PORT" ]]; then
|
|
||||||
sed -i "s/8000/$TA_PORT/g" /etc/nginx/sites-available/default
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -n "$TA_UWSGI_PORT" ]]; then
|
|
||||||
sed -i "s/8080/$TA_UWSGI_PORT/g" /etc/nginx/sites-available/default
|
|
||||||
sed -i "s/8080/$TA_UWSGI_PORT/g" /app/uwsgi.ini
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 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 &
|
|
||||||
celery -A home beat --loglevel=INFO \
|
|
||||||
-s "${BEAT_SCHEDULE_PATH:-/cache/celerybeat-schedule}" &
|
|
||||||
uwsgi --ini uwsgi.ini
|
|
@ -1,8 +0,0 @@
|
|||||||
[uwsgi]
|
|
||||||
module = config.wsgi:application
|
|
||||||
master = True
|
|
||||||
pidfile = /tmp/project-master.pid
|
|
||||||
vacuum = True
|
|
||||||
max-requests = 5000
|
|
||||||
socket = :8080
|
|
||||||
buffer-size = 8192
|
|
@ -1,32 +0,0 @@
|
|||||||
# Channels Overview and Channel Detail Page
|
|
||||||
|
|
||||||
The channels are organized on two different levels, similar as the [playlists](Playlists):
|
|
||||||
|
|
||||||
## 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 with the toggle. 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, if you aren't subscribed, a *Subscribe* button will show instead.
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
You can search your indexed channels by clicking on the search icon <img src="assets/icon-search.png?raw=true" alt="search icon" width="20px" style="margin:0 5px;">. This will open a dedicated page.
|
|
||||||
|
|
||||||
## 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, else the *Subscribe* 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 additionally this will also delete playlists metadata belonging to that channel.
|
|
||||||
- The button **Show Playlists** will go to the [playlists](Playlists) page and filter the list to only show playlists from this channel.
|
|
||||||
|
|
||||||
### Channel Customize
|
|
||||||
Clicking on the *Configure* button will open a form with options to configure settings on a per channel basis. Any configurations here will overwrite your settings from the [settings](Settings) page.
|
|
||||||
- **Download Format**: Overwrite the download qualities for videos from this channel.
|
|
||||||
- **Auto Delete**: Automatically delete watched videos from this channel after selected days.
|
|
||||||
- **Index Playlists**: Automatically add all Playlists with at least a video downloaded to your index. Only do this for channels where you care about playlists as this will slow down indexing new videos for having to check which playlist this belongs to.
|
|
||||||
- **SponsorBlock**: Using [SponsorBlock](https://sponsor.ajay.app/) to get and skip sponsored content. Customize per channel: You can *disable* or *enable* SponsorBlock for certain channels only to overwrite the behavior set on the [Settings](settings) page. Selecting *unset* will remove the overwrite and your setting will fall back to the default on the settings page.
|
|
@ -1,43 +0,0 @@
|
|||||||
# 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 and playlists you are subscribed to. You can define the channel and playlist page size on the [settings page](Settings#subscriptions). With the default page size, expect this process to take around 2-3 seconds for each channel or playlist 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 2 seconds 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.
|
|
||||||
|
|
||||||
After downloading, Tube Archivist tries to add new videos to already indexed playlists.
|
|
||||||
|
|
||||||
## 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: When adding a playlist to the queue, this playlist will automatically get [indexed](Playlists#playlist-detail).
|
|
||||||
- Note: 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.
|
|
||||||
|
|
||||||
The button **Delete all queued** will delete all pending videos from the download queue.
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
The button **Delete all ignored** will delete all videos you have previously ignored.
|
|
34
docs/FAQ.md
@ -1,34 +0,0 @@
|
|||||||
# Frequently Asked Questions
|
|
||||||
|
|
||||||
## 1. Scope of this project
|
|
||||||
Tube Archivist is *Your self hosted YouTube media server*, which also defines the primary scope of what this project tries to do:
|
|
||||||
- **Self hosted**: This assumes you have full control over the underlying operating system and hardware and can configure things to work properly with Docker, it's volumes and networks as well as whatever disk storage and filesystem you choose to use.
|
|
||||||
- **YouTube**: Downloading, indexing and playing videos from YouTube, there are currently no plans to expand this to any additional platforms.
|
|
||||||
- **Media server**: This project tries to be a stand alone media server in it's own web interface.
|
|
||||||
|
|
||||||
Additionally to that, progress is also happening on:
|
|
||||||
- **API**: Endpoints for additional integrations.
|
|
||||||
- **Browser Extension**: To integrate between youtube.com and Tube Archivist.
|
|
||||||
|
|
||||||
Defining the scope is important for the success of any project:
|
|
||||||
- A scope too broad will result in development effort spreading too thin and will run into danger that his project tries to do too many things and none of them well.
|
|
||||||
- A too narrow scope will make this project uninteresting and will exclude audiences that could also benefit from this project.
|
|
||||||
- Not defining a scope will easily lead to misunderstandings and false hopes of where this project tries to go.
|
|
||||||
|
|
||||||
Of course this is subject to change, as this project continues to grow and more people contribute.
|
|
||||||
|
|
||||||
## 2. Emby/Plex/Jellyfin/Kodi integrations
|
|
||||||
Although there are similarities between these excellent projects and Tube Archivist, they have a very different use case. Trying to fit the metadata relations and database structure of a YouTube archival project into these media servers that specialize in Movies and TV shows is always going to be limiting.
|
|
||||||
|
|
||||||
Part of the scope is to be its own media server, so that's where the focus and effort of this project is. That being said, the nature of self hosted and open source software gives you all the possible freedom to use your media as you wish.
|
|
||||||
|
|
||||||
## 3. To Docker or not to Docker
|
|
||||||
This project is a classical docker application: There are multiple moving parts that need to be able to interact with each other and need to be compatible with multiple architectures and operating systems. Additionally Docker also drastically reduces development complexity which is highly appreciated.
|
|
||||||
|
|
||||||
So Docker is the only supported installation method. If you don't have any experience with Docker, consider investing the time to learn this very useful technology.
|
|
||||||
|
|
||||||
## 4. Finetuning Elasticsearch
|
|
||||||
A minimal configuration of Elasticsearch (ES) is provided in the example docker-compose.yml file. ES is highly configurable and very interesting to learn more about. Refer to the [documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html) if you want to get into it.
|
|
||||||
|
|
||||||
## 5. Advanced Authentication
|
|
||||||
If you like to use things like SSO, LDAP or 2FA to login, consider using something like Authelia as a reverse proxy so this project can focus on the core task. Tube Archivist has a *remember me* checkbox at login to extend your sessions lifetime in your browser.
|
|
31
docs/Home.md
@ -1,31 +0,0 @@
|
|||||||
# Tube Archivist Wiki
|
|
||||||
Welcome to the official Tube Archivist Wiki. This is an up-to-date documentation of user functionality.
|
|
||||||
|
|
||||||
Table of contents:
|
|
||||||
* [FAQ](FAQ): Frequently asked questions what this project is and tries to do
|
|
||||||
* [Channels](Channels): Browse your channels, handle channel subscriptions
|
|
||||||
* [Playlists](Playlists): Browse your indexed playlists, handle playlist subscriptions
|
|
||||||
* [Downloads](Downloads): Scanning subscriptions, handle download queue
|
|
||||||
* [Settings](Settings): All the configuration options
|
|
||||||
* [Video](Video): All details of a single video and playlist navigation.
|
|
||||||
* [Users](Users): User management admin interface
|
|
||||||
* [Installation](Installation): WIP - detailed installation instructions for various platforms.
|
|
||||||
|
|
||||||
## 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.
|
|
||||||
* Clicking on the search icon <img src="assets/icon-search.png?raw=true" alt="gridview icon" width="20px" style="margin:0 5px;"> will open a dedicated search page to search over your complete index.
|
|
||||||
* The pagination - if available - builds links for up to 10'000 results, use the search, sort or filter functionality to find what you are looking for.
|
|
||||||
|
|
||||||
|
|
||||||
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. The sort icon <img src="assets/icon-sort.png?raw=true" alt="listview icon" width="20px" style="margin:0 5px;"> will open additional sort options.
|
|
@ -1,56 +0,0 @@
|
|||||||
# Detailed Installation Instructions for Various Platforms
|
|
||||||
|
|
||||||
## Unraid
|
|
||||||
|
|
||||||
Tube Archivist, and all if it's dependencies are located in the [community applications](https://forums.unraid.net/topic/38582-plug-in-community-applications/) store. The three containers you will need are as follows:
|
|
||||||
|
|
||||||
- **TubeArchivist-RedisJSON**: This container acts as a cache and temporary link between the application and the file system. Used to store and display messages and configuration variables.
|
|
||||||
- **TubeArchivist-ES**: ElasticSearch stores video meta data and makes everything searchable. Also keeps track of the download queue.
|
|
||||||
- **TubeArchivist**: 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.
|
|
||||||
|
|
||||||
### Step 1: Install `TubeArchivist-RedisJSON`
|
|
||||||
|
|
||||||
![enter image description here](https://i.imgur.com/ycAqFRU.png)
|
|
||||||
This is the easiest container to setup of the thee, just make sure that you do not have any port conflicts, and that your `/data` is mounted to the correct path. The other containers will map to the same directory.
|
|
||||||
|
|
||||||
If you need to install `TubeArchivist-RedisJSON`on a different port, you'll have to follow [these steps](https://github.com/tubearchivist/tubearchivist#redis-on-a-custom-port) later on when installing the `TubeArchivist` container
|
|
||||||
|
|
||||||
|
|
||||||
### Step 2: Install `TubeArchivist-ES`
|
|
||||||
![enter image description here](https://i.imgur.com/o6tsTdt.png)
|
|
||||||
ElasticSeach is also pretty easy to setup. Again, make sure you have no port conflicts, make sure that you mapped `/usr/share/elasticsearch/data` to the same directory as `RedisJSON`, and make sure to change the default password to something more secure.
|
|
||||||
|
|
||||||
There is three additional settings in the "show more settings" area, but leave those as they are.
|
|
||||||
|
|
||||||
|
|
||||||
### Step 3: Install `TubeArchivist`
|
|
||||||
|
|
||||||
![enter image description here](https://i.imgur.com/dwSCfgO.png)
|
|
||||||
It's finally time to set up TubeArchivist!
|
|
||||||
|
|
||||||
- `Port:`Again, make sure that you have no port conflicts on 8000.
|
|
||||||
|
|
||||||
- `Youtube Media Path:` is where you'll download all of your videos to.
|
|
||||||
Make sure that this is an empty directory to not cause confusion when
|
|
||||||
starting the application. If you have existing videos that you'd like
|
|
||||||
to import into Tube Archivist, please checkout the [settings
|
|
||||||
wiki.](https://github.com/tubearchivist/tubearchivist/wiki/Settings#manual-media-files-import)
|
|
||||||
|
|
||||||
|
|
||||||
- `Appdata:` This should be the same base path as the other two containers.
|
|
||||||
|
|
||||||
- `TA Username:`This will be your username for TubeArchivist.
|
|
||||||
|
|
||||||
- `TA Password:`This will be your password for TubeArchivist.
|
|
||||||
|
|
||||||
- `Redis` This will be JUST the ip address of your redis container
|
|
||||||
|
|
||||||
- `ElasticSearch Password:`This is the password you defined in the `TubeArchivist-ES` container.
|
|
||||||
- `ElasticSearch:` This seems to cause some confusion, but it's a pretty simple step, just replace the IP and Port to match you `TubeArchivist-ES` container.
|
|
||||||
|
|
||||||
(example: if your IP is 192.168.1.15, the value should be http://192.168.1.15:9200)
|
|
||||||
|
|
||||||
- `Time Zone:` This is an important step for your scheduler, to find your timezone, use a site like [TimeZoneConverter](http://www.timezoneconverter.com/cgi-bin/findzone.tzc)
|
|
||||||
|
|
||||||
### From there, you should be able to start up your containers and you're good to go!
|
|
||||||
If you're still having trouble, join us on [discord](https://discord.gg/AFwz8nE7BK) and come to the #unraid channel.
|
|
@ -1,23 +0,0 @@
|
|||||||
# Playlist Overview and Playlist Detail Page
|
|
||||||
The playlists are organized in two different levels, similar as the [channels](Channels):
|
|
||||||
|
|
||||||
## Playlist Overview
|
|
||||||
Accessible at `/playlist/` of your Tube Archivist, this **Overview Page** shows a list of all playlists you have indexed over all your channels.
|
|
||||||
- You can filter that list to show only subscribed to playlists with the toggle.
|
|
||||||
|
|
||||||
You can index playlists of a channel from the channel detail page as described [here](Channels#channel-detail).
|
|
||||||
|
|
||||||
The **Subscribe to Playlist** 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 playlists. You have a few options:
|
|
||||||
- Enter the YouTube playlist id, for example: *PL96C35uN7xGLLeET0dOWaKHkAlPsrkcha*
|
|
||||||
- Enter the Youtube dedicated playlist url, for example: *https://www.youtube.com/playlist?list=PL96C35uN7xGLLeET0dOWaKHkAlPsrkcha*
|
|
||||||
- Add one per line.
|
|
||||||
- NOTE: It doesn't make sense to subscribe to a playlist if you are already subscribed the corresponding channel as this will slow down the **Rescan Subscriptions** [task](Downloads#rescan-subscriptions).
|
|
||||||
|
|
||||||
You can search your indexed playlists by clicking on the search icon <img src="assets/icon-search.png?raw=true" alt="search icon" width="20px" style="margin:0 5px;">. This will open a dedicated page.
|
|
||||||
|
|
||||||
## Playlist Detail
|
|
||||||
Each playlist will get a dedicated playlist detail page accessible at `/playlist/<playlist-id>/` of your Tube Archivist. This page shows all the videos you have downloaded from this playlist.
|
|
||||||
|
|
||||||
- If you are subscribed to the playlist, an Unsubscribe button will show, else the Subscribe button will show.
|
|
||||||
- The Mark as Watched button will mark all videos of this playlist as watched.
|
|
||||||
- The **Delete Playlist** button will give you the option to delete just the *metadata* which won't delete any media files or *delete all* which will delete metadata plus all videos belonging to this playlist.
|
|
136
docs/Settings.md
@ -1,136 +0,0 @@
|
|||||||
# 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.
|
|
||||||
- **Auto Delete Watched Videos**: Automatically delete videos marked as watched after selected days. If activated, checks your videos after download task is finished.
|
|
||||||
|
|
||||||
## 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. Similar to that, `--check-formats` is passed as well to check that the selected formats are actually downloadable.
|
|
||||||
- **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.
|
|
||||||
|
|
||||||
## Subtitles
|
|
||||||
- **Download Setting**: Select the subtitle language you like to download. Add a comma separated list for multiple languages.
|
|
||||||
- **Source Settings**: User created subtitles are provided from the uploader and are usually the video script. Auto generated is from YouTube, quality varies, particularly for auto translated tracks.
|
|
||||||
- **Index Settings**: Enabling subtitle indexing will add the lines to Elasticsearch and will make subtitles searchable. This will increase the index size and is not recommended on low-end hardware.
|
|
||||||
|
|
||||||
## Integrations
|
|
||||||
All third party integrations of TubeArchivist will **always** be *opt in*.
|
|
||||||
- **API**: Your access token for the Tube Archivist API.
|
|
||||||
- **returnyoutubedislike.com**: This will get return dislikes and average ratings for each video by integrating with the API from [returnyoutubedislike.com](https://www.returnyoutubedislike.com/).
|
|
||||||
- **SponsorBlock**: Using [SponsorBlock](https://sponsor.ajay.app/) to get and skip sponsored content. If a video doesn't have timestamps, or has unlocked timestamps, use the browser addon to contribute to this excellent project. Can also be activated and deactivated as a per [channel overwrite](Settings#channel-customize).
|
|
||||||
- **Cast**: Enabling the cast integration in the settings page will load an additional JS library from **Google**.
|
|
||||||
* Requirements
|
|
||||||
- HTTPS
|
|
||||||
* To use the cast integration HTTPS needs to be enabled, which can be done using a reverse proxy. This is a requirement by Google as communication to the cast device is required to be encrypted, but the content itself is not.
|
|
||||||
- Supported Browser
|
|
||||||
* A supported browser is required for this integration such as Google Chrome. Other browsers, especially Chromium-based browsers, may support casting by enabling it in the settings.
|
|
||||||
- Subtitles
|
|
||||||
* Subtitles are supported however they do not work out of the box and require additional configuration. Due to requirements by Google, to use subtitles you need additional headers which will need to be configured in your reverse proxy. See this [page](https://developers.google.com/cast/docs/web_sender/advanced#cors_requirements) for the specific requirements.
|
|
||||||
> You need the following headers: Content-Type, Accept-Encoding, and Range. Note that the last two headers, Accept-Encoding and Range, are additional headers that you may not have needed previously.
|
|
||||||
> Wildcards "*" cannot be used for the Access-Control-Allow-Origin header. If the page has protected media content, it must use a domain instead of a wildcard.
|
|
||||||
|
|
||||||
|
|
||||||
# Scheduler Setup
|
|
||||||
Schedule settings expect a cron like format, where the first value is minute, second is hour and third is day of the week. Day 0 is Sunday, day 1 is Monday etc.
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
- **0 15 \***: Run task every day at 15:00 in the afternoon.
|
|
||||||
- **30 8 \*/2**: Run task every second day of the week (Sun, Tue, Thu, Sat) at 08:30 in the morning.
|
|
||||||
- **0 \*/3,8-17 \***: Execute every hour divisible by 3, and every hour during office hours (8 in the morning - 5 in the afternoon).
|
|
||||||
- **0 8,16 \***: Execute every day at 8 in the morning and at 4 in the afternoon.
|
|
||||||
- **auto**: Sensible default.
|
|
||||||
- **0**: (zero), deactivate that task.
|
|
||||||
|
|
||||||
NOTE:
|
|
||||||
- Changes in the scheduler settings require a container restart to take effect.
|
|
||||||
- Cron format as *number*/*number* are none standard cron and are not supported by the scheduler, for example **0 0/12 \*** is invalid, use **0 \*/12 \*** instead.
|
|
||||||
- Avoid an unnecessary frequent schedule to not get blocked by YouTube. For that reason * or wildcards for minutes are not supported.
|
|
||||||
|
|
||||||
## Rescan Subscriptions
|
|
||||||
That's the equivalent task as run from the downloads page looking through your channel and playlist and add missing videos to the download queue.
|
|
||||||
|
|
||||||
## Start download
|
|
||||||
Start downloading all videos currently in the download queue.
|
|
||||||
|
|
||||||
## Refresh Metadata
|
|
||||||
Rescan videos, channels and playlists on youtube and update metadata periodically. This will also deactivate an item and exclude it from future refreshes if the link on YouTube is no longer available. This task is meant to be run once per day, set your schedule accordingly.
|
|
||||||
|
|
||||||
The field **Refresh older than x days** takes a number where TubeArchivist will consider an item as *outdated*. This value is used to calculate how many items need to be refreshed today based on the total indexed. This will spread out the requests to YouTube. Sensible value here is **90** days.
|
|
||||||
|
|
||||||
## Thumbnail check
|
|
||||||
This will check if all expected thumbnails are there and will delete any artwork without matching video.
|
|
||||||
|
|
||||||
## Index backup
|
|
||||||
Create a zip file of the metadata and select **Max auto backups to keep** to automatically delete old backups created from this task.
|
|
||||||
|
|
||||||
|
|
||||||
# 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'd 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.
|
|
||||||
- For best file transcoding quality, convert your media files with desired settings first before importing (#138).
|
|
||||||
- There should be no subdirectories added to */cache/import*, only video files. If your existing video library has video files inside subdirectories, you can get all the files into one directory by running `find ./ -mindepth 2 -type f -exec mv '{}' . \;` from the top-level directory of your existing video library. You can also delete any remaining empty subdirectories with `find ./ -mindepth 1 -type d -delete`.
|
|
||||||
- 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. There will be a list of all available backup to choose from. The *source* tag can have these different values:
|
|
||||||
- **manual**: For backups manually created from here on the settings page.
|
|
||||||
- **auto**: For backups automatically created via a sceduled task.
|
|
||||||
- **update**: For backups created after a Tube Archivist update due to changes in the index.
|
|
||||||
- **False**: Undefined.
|
|
||||||
|
|
||||||
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*.
|
|
||||||
-This will also check all of your thumbnails and download any that are missing.
|
|
||||||
|
|
||||||
BE AWARE: There is no undo.
|
|
@ -1,20 +0,0 @@
|
|||||||
# 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.
|
|
@ -1,11 +0,0 @@
|
|||||||
# Video Page
|
|
||||||
|
|
||||||
Every video downloaded gets a dedicated page accessible at `/video/<video-id>` of your Tube Archivist.
|
|
||||||
|
|
||||||
Clicking on the channel name or the channel icon will bring you to the dedicated channel detail [page](Channels#channel-detail).
|
|
||||||
|
|
||||||
The button **Delete Video** will delete that video including the media file.
|
|
||||||
|
|
||||||
When available, a playlist navigation will show at the bottom. Clicking on the playlist name will bring you to the dedicated [Playlist Detail](Playlists#playlist-detail) page showing all videos downloaded from that playlist. The number in square brackets indicates the position of the current video in that playlist.
|
|
||||||
|
|
||||||
Clicking on the next or previous video name or thumbnail will bring you to that dedicated video page.
|
|
Before Width: | Height: | Size: 58 KiB |
Before Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 69 KiB |
Before Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 4.3 KiB |
Before Width: | Height: | Size: 4.3 KiB |
Before Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 3.1 KiB |
Before Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 4.4 KiB |
Before Width: | Height: | Size: 5.1 KiB |
Before Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 2.2 KiB |
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "www",
|
"name": "tubearchivist-frontend",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 8.4 KiB |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 920 B After Width: | Height: | Size: 920 B |
Before Width: | Height: | Size: 747 B After Width: | Height: | Size: 747 B |
Before Width: | Height: | Size: 1012 B After Width: | Height: | Size: 1012 B |
Before Width: | Height: | Size: 830 B After Width: | Height: | Size: 830 B |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 891 B After Width: | Height: | Size: 891 B |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 959 B After Width: | Height: | Size: 959 B |
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 6.6 KiB |
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 51 KiB |
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 54 KiB |
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 61 KiB |
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 84 KiB |
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 56 KiB |
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 6.0 KiB |
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.4 KiB |
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.0 KiB |
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.5 KiB |
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.2 KiB |
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
Before Width: | Height: | Size: 212 KiB After Width: | Height: | Size: 212 KiB |
Before Width: | Height: | Size: 218 KiB After Width: | Height: | Size: 218 KiB |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |