16 Commits
v0.9 ... v0.9.1

Author SHA1 Message Date
meeb
2c1c45e829 bump to 0.9.1 2021-03-05 14:20:23 +11:00
meeb
c64f54bcb4 bump to 0.9.1 2021-03-05 14:16:17 +11:00
meeb
6ce55b0337 bump libs 2021-03-04 15:39:51 +11:00
meeb
d06c4beae0 bump upstream libs 2021-02-22 19:22:30 +11:00
meeb
db651e16b9 raise a manual exception when youtube-dl extract_info returns no data to trigger backoffs, increase backoff retry and timers, resolves #66 2021-02-22 13:24:11 +11:00
meeb
86068790ed missing import 2021-02-21 11:52:14 +11:00
meeb
ea72671351 add version details for http basic auth instructions 2021-02-21 11:45:56 +11:00
meeb
96b9eddf43 add a reset tasks cli command 2021-02-21 11:44:52 +11:00
meeb
bceefc8b01 skip media which has no publish date in locally stored metadata 2021-02-21 11:15:57 +11:00
meeb
820cc69937 typo 2021-02-19 14:40:41 +11:00
meeb
1e8711be51 add media items downloaded counter to sources list overview 2021-02-19 14:37:56 +11:00
meeb
e3423bc2d2 preserve media filter when toggling skipped media 2021-02-19 14:26:01 +11:00
meeb
6fbf72d0e7 optional basic HTTP authentication, resolves #62 2021-02-19 12:58:34 +11:00
meeb
d6852bf828 account for metadata loading as None for upload_date, resolves #59 2021-02-18 20:50:13 +11:00
meeb
f6f4f244d7 hide skipped media by default and add a show skipped media button 2021-02-18 19:34:44 +11:00
meeb
df35aa2a5f increase media per page, tweak pagination button layout 2021-02-18 19:21:41 +11:00
16 changed files with 252 additions and 67 deletions

View File

@@ -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
View File

@@ -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": {}

View File

@@ -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:
![Sync or create missing metadata files](https://github.com/meeb/tubesync/blob/main/docs/create-missing-metadata.md) ![Sync or create missing metadata files](https://github.com/meeb/tubesync/blob/main/docs/create-missing-metadata.md)
![Reset tasks from the command line](https://github.com/meeb/tubesync/blob/main/docs/reset-tasks.md)
# 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
View 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.

View File

@@ -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)

View File

@@ -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;

View File

@@ -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>

View 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')

View File

@@ -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:

View File

@@ -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():

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -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

View File

@@ -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

View File

@@ -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 = {}

View File

@@ -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