Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c1c45e829 | ||
|
|
c64f54bcb4 | ||
|
|
6ce55b0337 | ||
|
|
d06c4beae0 | ||
|
|
db651e16b9 | ||
|
|
86068790ed | ||
|
|
ea72671351 | ||
|
|
96b9eddf43 | ||
|
|
bceefc8b01 | ||
|
|
820cc69937 | ||
|
|
1e8711be51 | ||
|
|
e3423bc2d2 | ||
|
|
6fbf72d0e7 | ||
|
|
d6852bf828 | ||
|
|
f6f4f244d7 | ||
|
|
df35aa2a5f |
1
Pipfile
1
Pipfile
@@ -17,6 +17,7 @@ httptools = "*"
|
|||||||
youtube-dl = "*"
|
youtube-dl = "*"
|
||||||
django-background-tasks = "*"
|
django-background-tasks = "*"
|
||||||
requests = "*"
|
requests = "*"
|
||||||
|
django-basicauth = "*"
|
||||||
|
|
||||||
[requires]
|
[requires]
|
||||||
python_version = "3"
|
python_version = "3"
|
||||||
|
|||||||
89
Pipfile.lock
generated
89
Pipfile.lock
generated
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"hash": {
|
"hash": {
|
||||||
"sha256": "a4bb556fc61ee4583f9588980450b071814298ee4d1a1023fad149c14d14aaba"
|
"sha256": "f698e2853dec2d325d2d7e752620fc81d911022d394a57f2f8a9349ac2682752"
|
||||||
},
|
},
|
||||||
"pipfile-spec": 6,
|
"pipfile-spec": 6,
|
||||||
"requires": {
|
"requires": {
|
||||||
@@ -39,11 +39,11 @@
|
|||||||
},
|
},
|
||||||
"django": {
|
"django": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:169e2e7b4839a7910b393eec127fd7cbae62e80fa55f89c6510426abf673fe5f",
|
"sha256:32ce792ee9b6a0cbbec340123e229ac9f765dff8c2a4ae9247a14b2ba3a365a7",
|
||||||
"sha256:c6c0462b8b361f8691171af1fb87eceb4442da28477e12200c40420176206ba7"
|
"sha256:baf099db36ad31f970775d0be5587cc58a6256a6771a44eb795b554d45f211b8"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==3.1.6"
|
"version": "==3.1.7"
|
||||||
},
|
},
|
||||||
"django-appconf": {
|
"django-appconf": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@@ -59,6 +59,14 @@
|
|||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==1.2.5"
|
"version": "==1.2.5"
|
||||||
},
|
},
|
||||||
|
"django-basicauth": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:15e9e366f698f53c71b1e794dafea060f990a2ac556bae6b7330dd25324a091c",
|
||||||
|
"sha256:e5e47d1acdc1943bedcc1bf673059d6c15e257dfe9eef67a22fb824f79546c0d"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==0.5.3"
|
||||||
|
},
|
||||||
"django-compat": {
|
"django-compat": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:3ac9a3bedc56b9365d9eb241bc5157d0c193769bf995f9a78dc1bc24e7c2331b"
|
"sha256:3ac9a3bedc56b9365d9eb241bc5157d0c193769bf995f9a78dc1bc24e7c2331b"
|
||||||
@@ -134,41 +142,42 @@
|
|||||||
},
|
},
|
||||||
"pillow": {
|
"pillow": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:165c88bc9d8dba670110c689e3cc5c71dbe4bfb984ffa7cbebf1fac9554071d6",
|
"sha256:01bb0a34f1a6689b138c0089d670ae2e8f886d2666a9b2f2019031abdea673c4",
|
||||||
"sha256:1d208e670abfeb41b6143537a681299ef86e92d2a3dac299d3cd6830d5c7bded",
|
"sha256:07872f1d8421db5a3fe770f7480835e5e90fddb58f36c216d4a2ac0d594de474",
|
||||||
"sha256:22d070ca2e60c99929ef274cfced04294d2368193e935c5d6febfd8b601bf865",
|
"sha256:1022f8f6dc3c5b0dcf928f1c49ba2ac73051f576af100d57776e2b65c1f76a8d",
|
||||||
"sha256:2353834b2c49b95e1313fb34edf18fca4d57446675d05298bb694bca4b194174",
|
"sha256:14415e9e28410232370615dbde0cf0a00e526f522f665460344a5b96973a3086",
|
||||||
"sha256:39725acf2d2e9c17356e6835dccebe7a697db55f25a09207e38b835d5e1bc032",
|
"sha256:172acfaf00434a28dddfe592d83f2980e22e63c769ff4a448ddf7b7a38ffd165",
|
||||||
"sha256:3de6b2ee4f78c6b3d89d184ade5d8fa68af0848f9b6b6da2b9ab7943ec46971a",
|
"sha256:1c5e3c36f02c815766ae9dd91899b1c5b4652f2a37b7a51609f3bd467c0f11fb",
|
||||||
"sha256:47c0d93ee9c8b181f353dbead6530b26980fe4f5485aa18be8f1fd3c3cbc685e",
|
"sha256:292f2aa1ae5c5c1451cb4b558addb88c257411d3fd71c6cf45562911baffc979",
|
||||||
"sha256:5e2fe3bb2363b862671eba632537cd3a823847db4d98be95690b7e382f3d6378",
|
"sha256:2a40d7d4b17db87f5b9a1efc0aff56000e1d0d5ece415090c102aafa0ccbe858",
|
||||||
"sha256:604815c55fd92e735f9738f65dabf4edc3e79f88541c221d292faec1904a4b17",
|
"sha256:2f0d7034d5faae9a8d1019d152ede924f653df2ce77d3bba4ce62cd21b5f94ae",
|
||||||
"sha256:6c5275bd82711cd3dcd0af8ce0bb99113ae8911fc2952805f1d012de7d600a4c",
|
"sha256:33fdbd4f5608c852d97264f9d2e3b54e9e9959083d008145175b86100b275e5b",
|
||||||
"sha256:731ca5aabe9085160cf68b2dbef95fc1991015bc0a3a6ea46a371ab88f3d0913",
|
"sha256:3b13d89d97b551e02549d1f0edf22bed6acfd6fd2e888cd1e9a953bf215f0e81",
|
||||||
"sha256:7612520e5e1a371d77e1d1ca3a3ee6227eef00d0a9cddb4ef7ecb0b7396eddf7",
|
"sha256:3e759bcc03d6f39bc751e56d86bc87252b9a21c689a27c5ed753717a87d53a5b",
|
||||||
"sha256:7916cbc94f1c6b1301ac04510d0881b9e9feb20ae34094d3615a8a7c3db0dcc0",
|
"sha256:3ec87bd1248b23a2e4e19e774367fbe30fddc73913edc5f9b37470624f55dc1f",
|
||||||
"sha256:81c3fa9a75d9f1afafdb916d5995633f319db09bd773cb56b8e39f1e98d90820",
|
"sha256:436b0a2dd9fe3f7aa6a444af6bdf53c1eb8f5ced9ea3ef104daa83f0ea18e7bc",
|
||||||
"sha256:887668e792b7edbfb1d3c9d8b5d8c859269a0f0eba4dda562adb95500f60dbba",
|
"sha256:43b3c859912e8bf754b3c5142df624794b18eb7ae07cfeddc917e1a9406a3ef2",
|
||||||
"sha256:93a473b53cc6e0b3ce6bf51b1b95b7b1e7e6084be3a07e40f79b42e83503fbf2",
|
"sha256:4fe74636ee71c57a7f65d7b21a9f127d842b4fb75511e5d256ace258826eb352",
|
||||||
"sha256:96d4dc103d1a0fa6d47c6c55a47de5f5dafd5ef0114fa10c85a1fd8e0216284b",
|
"sha256:59445af66b59cc39530b4f810776928d75e95f41e945f0c32a3de4aceb93c15d",
|
||||||
"sha256:a3d3e086474ef12ef13d42e5f9b7bbf09d39cf6bd4940f982263d6954b13f6a9",
|
"sha256:69da5b1d7102a61ce9b45deb2920a2012d52fd8f4201495ea9411d0071b0ec22",
|
||||||
"sha256:b02a0b9f332086657852b1f7cb380f6a42403a6d9c42a4c34a561aa4530d5234",
|
"sha256:7094bbdecb95ebe53166e4c12cf5e28310c2b550b08c07c5dc15433898e2238e",
|
||||||
"sha256:b09e10ec453de97f9a23a5aa5e30b334195e8d2ddd1ce76cc32e52ba63c8b31d",
|
"sha256:8211cac9bf10461f9e33fe9a3af6c5131f3fdd0d10672afc2abb2c70cf95c5ca",
|
||||||
"sha256:b6f00ad5ebe846cc91763b1d0c6d30a8042e02b2316e27b05de04fa6ec831ec5",
|
"sha256:8cf77e458bd996dc85455f10fe443c0c946f5b13253773439bcbec08aa1aebc2",
|
||||||
"sha256:bba80df38cfc17f490ec651c73bb37cd896bc2400cfba27d078c2135223c1206",
|
"sha256:924fc33cb4acaf6267b8ca3b8f1922620d57a28470d5e4f49672cea9a841eb08",
|
||||||
"sha256:c3d911614b008e8a576b8e5303e3db29224b455d3d66d1b2848ba6ca83f9ece9",
|
"sha256:99ce3333b40b7a4435e0a18baad468d44ab118a4b1da0af0a888893d03253f1d",
|
||||||
"sha256:ca20739e303254287138234485579b28cb0d524401f83d5129b5ff9d606cb0a8",
|
"sha256:a7d690b2c5f7e4a932374615fedceb1e305d2dd5363c1de15961725fe10e7d16",
|
||||||
"sha256:cb192176b477d49b0a327b2a5a4979552b7a58cd42037034316b8018ac3ebb59",
|
"sha256:b9af590adc1e46898a1276527f3cfe2da8048ae43fbbf9b1bf9395f6c99d9b47",
|
||||||
"sha256:cdbbe7dff4a677fb555a54f9bc0450f2a21a93c5ba2b44e09e54fcb72d2bd13d",
|
"sha256:bb18422ad00c1fecc731d06592e99c3be2c634da19e26942ba2f13d805005cf2",
|
||||||
"sha256:cf6e33d92b1526190a1de904df21663c46a456758c0424e4f947ae9aa6088bf7",
|
"sha256:c10af40ee2f1a99e1ae755ab1f773916e8bca3364029a042cd9161c400416bd8",
|
||||||
"sha256:d355502dce85ade85a2511b40b4c61a128902f246504f7de29bbeec1ae27933a",
|
"sha256:c143c409e7bc1db784471fe9d0bf95f37c4458e879ad84cfae640cb74ee11a26",
|
||||||
"sha256:d673c4990acd016229a5c1c4ee8a9e6d8f481b27ade5fc3d95938697fa443ce0",
|
"sha256:c448d2b335e21951416a30cd48d35588d122a912d5fe9e41900afacecc7d21a1",
|
||||||
"sha256:dc577f4cfdda354db3ae37a572428a90ffdbe4e51eda7849bf442fb803f09c9b",
|
"sha256:d30f30c044bdc0ab8f3924e1eeaac87e0ff8a27e87369c5cac4064b6ec78fd83",
|
||||||
"sha256:dd9eef866c70d2cbbea1ae58134eaffda0d4bfea403025f4db6859724b18ab3d",
|
"sha256:df534e64d4f3e84e8f1e1a37da3f541555d947c1c1c09b32178537f0f243f69d",
|
||||||
"sha256:f50e7a98b0453f39000619d845be8b06e611e56ee6e8186f7f60c3b1e2f0feae"
|
"sha256:f6fc18f9c9c7959bf58e6faf801d14fafb6d4717faaf6f79a68c8bb2a13dcf20",
|
||||||
|
"sha256:ff83dfeb04c98bb3e7948f876c17513a34e9a19fd92e292288649164924c1b39"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==8.1.0"
|
"version": "==8.1.1"
|
||||||
},
|
},
|
||||||
"pytz": {
|
"pytz": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@@ -240,11 +249,11 @@
|
|||||||
},
|
},
|
||||||
"youtube-dl": {
|
"youtube-dl": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:b390cddbd4d605bd887d0d4063988cef0fa13f916d2e1e3564badbb22504d754",
|
"sha256:02432aa2dd0e859e64d74fca2ad624abf3bead3dba811d594100e1cb7897dce7",
|
||||||
"sha256:e7d48cd42f3081e1e0064e69f31f2856508ef31c0fc80eeebd8e70c6a031a24d"
|
"sha256:28663ce51bb35d0a0fa764aed3492b38c570da0a5a62fef3c28f4431522a6d4a"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==2021.2.10"
|
"version": "==2021.3.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"develop": {}
|
"develop": {}
|
||||||
|
|||||||
53
README.md
53
README.md
@@ -22,7 +22,7 @@ hopefully, quite reliable.
|
|||||||
# Latest container image
|
# Latest container image
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
ghcr.io/meeb/tubesync:v0.9
|
ghcr.io/meeb/tubesync:v0.9.1
|
||||||
```
|
```
|
||||||
|
|
||||||
**NOTE: the `:latest` tag does exist, but will contain in-development commits and may
|
**NOTE: the `:latest` tag does exist, but will contain in-development commits and may
|
||||||
@@ -102,7 +102,7 @@ Finally, download and run the container:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Pull a versioned image
|
# Pull a versioned image
|
||||||
$ docker pull ghcr.io/meeb/tubesync:v0.9
|
$ docker pull ghcr.io/meeb/tubesync:v0.9.1
|
||||||
# Start the container using your user ID and group ID
|
# Start the container using your user ID and group ID
|
||||||
$ docker run \
|
$ docker run \
|
||||||
-d \
|
-d \
|
||||||
@@ -113,7 +113,7 @@ $ docker run \
|
|||||||
-v /some/directory/tubesync-config:/config \
|
-v /some/directory/tubesync-config:/config \
|
||||||
-v /some/directory/tubesync-downloads:/downloads \
|
-v /some/directory/tubesync-downloads:/downloads \
|
||||||
-p 4848:4848 \
|
-p 4848:4848 \
|
||||||
ghcr.io/meeb/tubesync:v0.9
|
ghcr.io/meeb/tubesync:v0.9.1
|
||||||
```
|
```
|
||||||
|
|
||||||
Once running, open `http://localhost:4848` in your browser and you should see the
|
Once running, open `http://localhost:4848` in your browser and you should see the
|
||||||
@@ -125,7 +125,7 @@ Alternatively, for Docker Compose, you can use something like:
|
|||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
tubesync:
|
tubesync:
|
||||||
image: ghcr.io/meeb/tubesync:v0.9
|
image: ghcr.io/meeb/tubesync:v0.9.1
|
||||||
container_name: tubesync
|
container_name: tubesync
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
@@ -139,6 +139,41 @@ Alternatively, for Docker Compose, you can use something like:
|
|||||||
- PGID=1000
|
- PGID=1000
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Optional authentication
|
||||||
|
|
||||||
|
Available in `v1.0` (or `:latest`)and later. If you want to enable a basic username and
|
||||||
|
password to be required to access the TubeSync dashboard you can set them with the
|
||||||
|
following environment variables:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
HTTP_USER
|
||||||
|
HTTP_PASS
|
||||||
|
```
|
||||||
|
|
||||||
|
For example in the `docker run ...` line add in:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
...
|
||||||
|
-e HTTP_USER=some-username \
|
||||||
|
-e HTTP_PASS=some-secure-password \
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
Or in your Docker Compose file you would add in:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
...
|
||||||
|
environment:
|
||||||
|
- HTTP_USER=some-username
|
||||||
|
- HTTP_PASS=some-secure-password
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
When BOTH `HTTP_USER` and `HTTP_PASS` are set then basic HTTP authentication will be
|
||||||
|
enabled.
|
||||||
|
|
||||||
|
|
||||||
# Updating
|
# Updating
|
||||||
|
|
||||||
To update, you can just pull a new version of the container image as they are released.
|
To update, you can just pull a new version of the container image as they are released.
|
||||||
@@ -205,6 +240,8 @@ and less common features:
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
# Warnings
|
# Warnings
|
||||||
|
|
||||||
@@ -300,7 +337,9 @@ can log in at http://localhost:4848/admin
|
|||||||
|
|
||||||
### Are there user accounts or multi-user support?
|
### Are there user accounts or multi-user support?
|
||||||
|
|
||||||
No not at the moment. This could be added later if there is demand for it.
|
There is support for basic HTTP authentication by setting the `HTTP_USER` and
|
||||||
|
`HTTP_PASS` environment variables. There is not support for multi-user or user
|
||||||
|
management.
|
||||||
|
|
||||||
### Does TubeSync support HTTPS?
|
### Does TubeSync support HTTPS?
|
||||||
|
|
||||||
@@ -328,6 +367,8 @@ useful if you are manually installing TubeSync in some other environment. These
|
|||||||
| GUNICORN_WORKERS | Number of gunicorn workers to spawn | 3 |
|
| GUNICORN_WORKERS | Number of gunicorn workers to spawn | 3 |
|
||||||
| LISTEN_HOST | IP address for gunicorn to listen on | 127.0.0.1 |
|
| LISTEN_HOST | IP address for gunicorn to listen on | 127.0.0.1 |
|
||||||
| LISTEN_PORT | Port number for gunicorn to listen on | 8080 |
|
| LISTEN_PORT | Port number for gunicorn to listen on | 8080 |
|
||||||
|
| HTTP_USER | Sets the username for HTTP basic authentication | some-username |
|
||||||
|
| HTTP_PASS | Sets the password for HTTP basic authentication | some-secure-password |
|
||||||
|
|
||||||
|
|
||||||
# Manual, non-containerised, installation
|
# Manual, non-containerised, installation
|
||||||
@@ -344,7 +385,7 @@ installing and running WSGI-based Python web applications before attempting this
|
|||||||
`tubesync/tubesync/local_settings.py` and edit it as appropriate
|
`tubesync/tubesync/local_settings.py` and edit it as appropriate
|
||||||
5. Run migrations with `./manage.py migrate`
|
5. Run migrations with `./manage.py migrate`
|
||||||
6. Collect static files with `./manage.py collectstatic`
|
6. Collect static files with `./manage.py collectstatic`
|
||||||
6. Set up your prefered WSGI server, such as `gunicorn` poiting it to the application
|
6. Set up your prefered WSGI server, such as `gunicorn` pointing it to the application
|
||||||
in `tubesync/tubesync/wsgi.py`
|
in `tubesync/tubesync/wsgi.py`
|
||||||
7. Set up your proxy server such as `nginx` and forward it to the WSGI server
|
7. Set up your proxy server such as `nginx` and forward it to the WSGI server
|
||||||
8. Check the web interface is working
|
8. Check the web interface is working
|
||||||
|
|||||||
33
docs/reset-tasks.md
Normal file
33
docs/reset-tasks.md
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# TubeSync
|
||||||
|
|
||||||
|
## Advanced usage guide - reset tasks from the command line
|
||||||
|
|
||||||
|
This is a new feature in v1.0 of TubeSync and later. It allows you to reset all
|
||||||
|
scheduled tasks from the command line as well as the "reset tasks" button in the
|
||||||
|
"tasks" tab of the dashboard.
|
||||||
|
|
||||||
|
This is useful for TubeSync installations where you may have a lot of media and
|
||||||
|
sources added and the "reset tasks" button may take too long to the extent where
|
||||||
|
the page times out (with a 502 error or similar issue).
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
You have added some sources and media
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
|
||||||
|
### 1. Run the reset tasks command
|
||||||
|
|
||||||
|
Execute the following Django command:
|
||||||
|
|
||||||
|
`./manage.py reset-tasks`
|
||||||
|
|
||||||
|
When deploying TubeSync inside a container, you can execute this with:
|
||||||
|
|
||||||
|
`docker exec -ti tubesync python3 /app/manage.py reset-tasks`
|
||||||
|
|
||||||
|
This command will log what its doing to the terminal when you run it.
|
||||||
|
|
||||||
|
When this is run, new tasks will be immediately created so all your sources will be
|
||||||
|
indexed again straight away, any missing information such as thumbnails will be
|
||||||
|
redownloaded, etc.
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
|
from django.conf import settings
|
||||||
from django.forms import BaseForm
|
from django.forms import BaseForm
|
||||||
|
from basicauth.middleware import BasicAuthMiddleware as BaseBasicAuthMiddleware
|
||||||
|
|
||||||
|
|
||||||
class MaterializeDefaultFieldsMiddleware:
|
class MaterializeDefaultFieldsMiddleware:
|
||||||
@@ -19,3 +21,12 @@ class MaterializeDefaultFieldsMiddleware:
|
|||||||
for _, field in v.fields.items():
|
for _, field in v.fields.items():
|
||||||
field.widget.attrs.update({'class':'browser-default'})
|
field.widget.attrs.update({'class':'browser-default'})
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
class BasicAuthMiddleware(BaseBasicAuthMiddleware):
|
||||||
|
|
||||||
|
def process_request(self, request):
|
||||||
|
bypass_uris = getattr(settings, 'BASICAUTH_ALWAYS_ALLOW_URIS', [])
|
||||||
|
if request.path in bypass_uris:
|
||||||
|
return None
|
||||||
|
return super().process_request(request)
|
||||||
|
|||||||
@@ -181,8 +181,10 @@ main {
|
|||||||
display: inline-block;
|
display: inline-block;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
padding: 5px 10px 5px 10px;
|
padding: 5px 8px 4px 8px;
|
||||||
margin: 0 3px 0 3px;
|
margin: 0 3px 6px 3px;
|
||||||
|
min-width: 40px;
|
||||||
|
min-height: 40px;
|
||||||
background-color: $pagination-background-colour;
|
background-color: $pagination-background-colour;
|
||||||
color: $pagination-text-colour;
|
color: $pagination-text-colour;
|
||||||
border: 2px $pagination-border-colour solid;
|
border: 2px $pagination-border-colour solid;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<div class="col s12">
|
<div class="col s12">
|
||||||
<div class="pagination">
|
<div class="pagination">
|
||||||
{% for i in paginator.page_range %}
|
{% for i in paginator.page_range %}
|
||||||
<a class="pagenum{% if i == page_obj.number %} currentpage{% endif %}" href="?{% if filter %}filter={{ filter }}&{% endif %}page={{ i }}">{{ i }}</a>
|
<a class="pagenum{% if i == page_obj.number %} currentpage{% endif %}" href="?{% if filter %}filter={{ filter }}&{% endif %}page={{ i }}{% if show_skipped %}&show_skipped=yes{% endif %}">{{ i }}</a>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
32
tubesync/sync/management/commands/reset-tasks.py
Normal file
32
tubesync/sync/management/commands/reset-tasks.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
from django.core.management.base import BaseCommand, CommandError
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from background_task.models import Task
|
||||||
|
from sync.models import Source
|
||||||
|
from sync.tasks import index_source_task
|
||||||
|
|
||||||
|
|
||||||
|
from common.logger import log
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
|
||||||
|
help = 'Resets all tasks'
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
log.info('Resettings all tasks...')
|
||||||
|
# Delete all tasks
|
||||||
|
Task.objects.all().delete()
|
||||||
|
# Iter all tasks
|
||||||
|
for source in Source.objects.all():
|
||||||
|
# Recreate the initial indexing task
|
||||||
|
verbose_name = _('Index media from source "{}"')
|
||||||
|
index_source_task(
|
||||||
|
str(source.pk),
|
||||||
|
repeat=source.index_schedule,
|
||||||
|
queue=str(source.pk),
|
||||||
|
priority=5,
|
||||||
|
verbose_name=verbose_name.format(source.name)
|
||||||
|
)
|
||||||
|
# This also chains down to call each Media objects .save() as well
|
||||||
|
source.save()
|
||||||
|
log.info('Done')
|
||||||
@@ -931,7 +931,10 @@ class Media(models.Model):
|
|||||||
@property
|
@property
|
||||||
def loaded_metadata(self):
|
def loaded_metadata(self):
|
||||||
try:
|
try:
|
||||||
return json.loads(self.metadata)
|
data = json.loads(self.metadata)
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
return {}
|
||||||
|
return data
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
@@ -968,7 +971,10 @@ class Media(models.Model):
|
|||||||
@property
|
@property
|
||||||
def upload_date(self):
|
def upload_date(self):
|
||||||
field = self.get_metadata_field('upload_date')
|
field = self.get_metadata_field('upload_date')
|
||||||
upload_date_str = self.loaded_metadata.get(field, '').strip()
|
try:
|
||||||
|
upload_date_str = self.loaded_metadata.get(field, '').strip()
|
||||||
|
except (AttributeError, ValueError) as e:
|
||||||
|
return None
|
||||||
try:
|
try:
|
||||||
return datetime.strptime(upload_date_str, '%Y%m%d')
|
return datetime.strptime(upload_date_str, '%Y%m%d')
|
||||||
except (AttributeError, ValueError) as e:
|
except (AttributeError, ValueError) as e:
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import math
|
|||||||
import uuid
|
import uuid
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from hashlib import sha1
|
from hashlib import sha1
|
||||||
from datetime import timedelta
|
from datetime import timedelta, datetime
|
||||||
from shutil import copyfile
|
from shutil import copyfile
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@@ -242,12 +242,17 @@ def download_media_metadata(media_id):
|
|||||||
media.skip = True
|
media.skip = True
|
||||||
# If the source has a cut-off check the upload date is within the allowed delta
|
# If the source has a cut-off check the upload date is within the allowed delta
|
||||||
if source.delete_old_media and source.days_to_keep > 0:
|
if source.delete_old_media and source.days_to_keep > 0:
|
||||||
delta = timezone.now() - timedelta(days=source.days_to_keep)
|
if not isinstance(media.published, datetime):
|
||||||
if media.published < delta:
|
# Media has no known published date or incomplete metadata
|
||||||
# Media was published after the cutoff date, skip it
|
log.warn(f'Media: {source} / {media} has no published date, skipping')
|
||||||
log.warn(f'Media: {source} / {media} is older than '
|
|
||||||
f'{source.days_to_keep} days, skipping')
|
|
||||||
media.skip = True
|
media.skip = True
|
||||||
|
else:
|
||||||
|
delta = timezone.now() - timedelta(days=source.days_to_keep)
|
||||||
|
if media.published < delta:
|
||||||
|
# Media was published after the cutoff date, skip it
|
||||||
|
log.warn(f'Media: {source} / {media} is older than '
|
||||||
|
f'{source.days_to_keep} days, skipping')
|
||||||
|
media.skip = True
|
||||||
# Check we can download the media item
|
# Check we can download the media item
|
||||||
if not media.skip:
|
if not media.skip:
|
||||||
if media.get_format_str():
|
if media.get_format_str():
|
||||||
|
|||||||
@@ -4,9 +4,16 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col s12">
|
<div class="col s12 m9">
|
||||||
<h1 class="truncate">Media</h1>
|
<h1 class="truncate">Media</h1>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col s12 m3">
|
||||||
|
{% if show_skipped %}
|
||||||
|
<a href="{% url 'sync:media' %}{% if source %}?filter={{ source.pk }}{% endif %}" class="btn"><i class="far fa-eye-slash"></i> Hide skipped media</a>
|
||||||
|
{% else %}
|
||||||
|
<a href="{% url 'sync:media' %}?show_skipped=yes{% if source %}&filter={{ source.pk }}{% endif %}" class="btn"><i class="far fa-eye"></i> Show skipped media</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% include 'infobox.html' with message=message %}
|
{% include 'infobox.html' with message=message %}
|
||||||
<div class="row no-margin-bottom">
|
<div class="row no-margin-bottom">
|
||||||
@@ -48,5 +55,5 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% include 'pagination.html' with pagination=sources.paginator filter=source.pk %}
|
{% include 'pagination.html' with pagination=sources.paginator filter=source.pk show_skipped=show_skipped %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
{% if source.has_failed %}
|
{% if source.has_failed %}
|
||||||
<span class="error-text"><i class="fas fa-exclamation-triangle"></i> <strong>Source has permanent failures</strong></span>
|
<span class="error-text"><i class="fas fa-exclamation-triangle"></i> <strong>Source has permanent failures</strong></span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<strong>{{ source.media_count }}</strong> media items{% if source.delete_old_media and source.days_to_keep > 0 %}, keep {{ source.days_to_keep }} days of media{% endif %}
|
<strong>{{ source.media_count }}</strong> media items, <strong>{{ source.downloaded_count }}</strong> downloaded{% if source.delete_old_media and source.days_to_keep > 0 %}, keeping {{ source.days_to_keep }} days of media{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</a>
|
</a>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from django.views.generic.detail import SingleObjectMixin
|
|||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.db import IntegrityError
|
from django.db import IntegrityError
|
||||||
from django.db.models import Q, Count, Sum
|
from django.db.models import Q, Count, Sum, When, Case
|
||||||
from django.forms import ValidationError
|
from django.forms import ValidationError
|
||||||
from django.utils.text import slugify
|
from django.utils.text import slugify
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
@@ -104,7 +104,10 @@ class SourcesView(ListView):
|
|||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
all_sources = Source.objects.all().order_by('name')
|
all_sources = Source.objects.all().order_by('name')
|
||||||
return all_sources.annotate(media_count=Count('media_source'))
|
return all_sources.annotate(
|
||||||
|
media_count=Count('media_source'),
|
||||||
|
downloaded_count=Count(Case(When(media_source__downloaded=True, then=1)))
|
||||||
|
)
|
||||||
|
|
||||||
def get_context_data(self, *args, **kwargs):
|
def get_context_data(self, *args, **kwargs):
|
||||||
data = super().get_context_data(*args, **kwargs)
|
data = super().get_context_data(*args, **kwargs)
|
||||||
@@ -438,6 +441,7 @@ class MediaView(ListView):
|
|||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self.filter_source = None
|
self.filter_source = None
|
||||||
|
self.show_skipped = False
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
@@ -447,13 +451,22 @@ class MediaView(ListView):
|
|||||||
self.filter_source = Source.objects.get(pk=filter_by)
|
self.filter_source = Source.objects.get(pk=filter_by)
|
||||||
except Source.DoesNotExist:
|
except Source.DoesNotExist:
|
||||||
self.filter_source = None
|
self.filter_source = None
|
||||||
|
show_skipped = request.GET.get('show_skipped', '').strip()
|
||||||
|
if show_skipped == 'yes':
|
||||||
|
self.show_skipped = True
|
||||||
return super().dispatch(request, *args, **kwargs)
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
if self.filter_source:
|
if self.filter_source:
|
||||||
q = Media.objects.filter(source=self.filter_source)
|
if self.show_skipped:
|
||||||
|
q = Media.objects.filter(source=self.filter_source)
|
||||||
|
else:
|
||||||
|
q = Media.objects.filter(source=self.filter_source, skip=False)
|
||||||
else:
|
else:
|
||||||
q = Media.objects.all()
|
if self.show_skipped:
|
||||||
|
q = Media.objects.all()
|
||||||
|
else:
|
||||||
|
q = Media.objects.filter(skip=False)
|
||||||
return q.order_by('-published', '-created')
|
return q.order_by('-published', '-created')
|
||||||
|
|
||||||
def get_context_data(self, *args, **kwargs):
|
def get_context_data(self, *args, **kwargs):
|
||||||
@@ -464,6 +477,7 @@ class MediaView(ListView):
|
|||||||
message = str(self.messages.get('filter', ''))
|
message = str(self.messages.get('filter', ''))
|
||||||
data['message'] = message.format(name=self.filter_source.name)
|
data['message'] = message.format(name=self.filter_source.name)
|
||||||
data['source'] = self.filter_source
|
data['source'] = self.filter_source
|
||||||
|
data['show_skipped'] = self.show_skipped
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -46,6 +46,11 @@ def get_media_info(url):
|
|||||||
response = y.extract_info(url, download=False)
|
response = y.extract_info(url, download=False)
|
||||||
except youtube_dl.utils.DownloadError as e:
|
except youtube_dl.utils.DownloadError as e:
|
||||||
raise YouTubeError(f'Failed to extract_info for "{url}": {e}') from e
|
raise YouTubeError(f'Failed to extract_info for "{url}": {e}') from e
|
||||||
|
if not response:
|
||||||
|
raise YouTubeError(f'Failed to extract_info for "{url}": No metadata was '
|
||||||
|
f'returned by youtube-dl, check for error messages in the '
|
||||||
|
f'logs above. This task will be retried later with an '
|
||||||
|
f'exponential backoff.')
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -38,3 +38,15 @@ if BACKGROUND_TASK_ASYNC_THREADS > MAX_BACKGROUND_TASK_ASYNC_THREADS:
|
|||||||
MEDIA_ROOT = CONFIG_BASE_DIR / 'media'
|
MEDIA_ROOT = CONFIG_BASE_DIR / 'media'
|
||||||
DOWNLOAD_ROOT = DOWNLOADS_BASE_DIR
|
DOWNLOAD_ROOT = DOWNLOADS_BASE_DIR
|
||||||
YOUTUBE_DL_CACHEDIR = CONFIG_BASE_DIR / 'cache'
|
YOUTUBE_DL_CACHEDIR = CONFIG_BASE_DIR / 'cache'
|
||||||
|
|
||||||
|
|
||||||
|
BASICAUTH_USERNAME = os.getenv('HTTP_USER', '').strip()
|
||||||
|
BASICAUTH_PASSWORD = os.getenv('HTTP_PASS', '').strip()
|
||||||
|
if BASICAUTH_USERNAME and BASICAUTH_PASSWORD:
|
||||||
|
BASICAUTH_DISABLE = False
|
||||||
|
BASICAUTH_USERS = {
|
||||||
|
BASICAUTH_USERNAME: BASICAUTH_PASSWORD,
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
BASICAUTH_DISABLE = True
|
||||||
|
BASICAUTH_USERS = {}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ CONFIG_BASE_DIR = BASE_DIR
|
|||||||
DOWNLOADS_BASE_DIR = BASE_DIR
|
DOWNLOADS_BASE_DIR = BASE_DIR
|
||||||
|
|
||||||
|
|
||||||
VERSION = 0.9
|
VERSION = '0.9.1'
|
||||||
SECRET_KEY = ''
|
SECRET_KEY = ''
|
||||||
DEBUG = False
|
DEBUG = False
|
||||||
ALLOWED_HOSTS = []
|
ALLOWED_HOSTS = []
|
||||||
@@ -37,6 +37,7 @@ MIDDLEWARE = [
|
|||||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
'whitenoise.middleware.WhiteNoiseMiddleware',
|
'whitenoise.middleware.WhiteNoiseMiddleware',
|
||||||
'common.middleware.MaterializeDefaultFieldsMiddleware',
|
'common.middleware.MaterializeDefaultFieldsMiddleware',
|
||||||
|
'common.middleware.BasicAuthMiddleware',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -117,11 +118,17 @@ Disallow: /
|
|||||||
X_FRAME_OPTIONS = 'SAMEORIGIN'
|
X_FRAME_OPTIONS = 'SAMEORIGIN'
|
||||||
|
|
||||||
|
|
||||||
|
BASICAUTH_DISABLE = True
|
||||||
|
BASICAUTH_REALM = 'Authenticate to TubeSync'
|
||||||
|
BASICAUTH_ALWAYS_ALLOW_URIS = ('/healthcheck',)
|
||||||
|
BASICAUTH_USERS = {}
|
||||||
|
|
||||||
|
|
||||||
HEALTHCHECK_FIREWALL = True
|
HEALTHCHECK_FIREWALL = True
|
||||||
HEALTHCHECK_ALLOWED_IPS = ('127.0.0.1',)
|
HEALTHCHECK_ALLOWED_IPS = ('127.0.0.1',)
|
||||||
|
|
||||||
|
|
||||||
MAX_ATTEMPTS = 10 # Number of times tasks will be retried
|
MAX_ATTEMPTS = 15 # Number of times tasks will be retried
|
||||||
MAX_RUN_TIME = 1800 # Maximum amount of time in seconds a task can run
|
MAX_RUN_TIME = 1800 # Maximum amount of time in seconds a task can run
|
||||||
BACKGROUND_TASK_RUN_ASYNC = True # Run tasks async in the background
|
BACKGROUND_TASK_RUN_ASYNC = True # Run tasks async in the background
|
||||||
BACKGROUND_TASK_ASYNC_THREADS = 1 # Number of async tasks to run at once
|
BACKGROUND_TASK_ASYNC_THREADS = 1 # Number of async tasks to run at once
|
||||||
@@ -131,7 +138,7 @@ COMPLETED_TASKS_DAYS_TO_KEEP = 7 # Number of days to keep completed t
|
|||||||
|
|
||||||
|
|
||||||
SOURCES_PER_PAGE = 100
|
SOURCES_PER_PAGE = 100
|
||||||
MEDIA_PER_PAGE = 72
|
MEDIA_PER_PAGE = 144
|
||||||
TASKS_PER_PAGE = 100
|
TASKS_PER_PAGE = 100
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user