Big reorg to support pacakging
This commit is contained in:
0
vrobbler/apps/scrobbles/__init__.py
Normal file
0
vrobbler/apps/scrobbles/__init__.py
Normal file
19
vrobbler/apps/scrobbles/admin.py
Normal file
19
vrobbler/apps/scrobbles/admin.py
Normal file
@ -0,0 +1,19 @@
|
||||
from django.contrib import admin
|
||||
|
||||
from scrobbles.models import Scrobble
|
||||
|
||||
|
||||
class ScrobbleAdmin(admin.ModelAdmin):
|
||||
date_hierarchy = "timestamp"
|
||||
list_display = (
|
||||
"video",
|
||||
"timestamp",
|
||||
"source",
|
||||
"playback_position",
|
||||
"in_progress",
|
||||
)
|
||||
list_filter = ("video",)
|
||||
ordering = ("-timestamp",)
|
||||
|
||||
|
||||
admin.site.register(Scrobble, ScrobbleAdmin)
|
||||
5
vrobbler/apps/scrobbles/apps.py
Normal file
5
vrobbler/apps/scrobbles/apps.py
Normal file
@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ScrobblesConfig(AppConfig):
|
||||
name = 'scrobbles'
|
||||
81
vrobbler/apps/scrobbles/migrations/0001_initial.py
Normal file
81
vrobbler/apps/scrobbles/migrations/0001_initial.py
Normal file
@ -0,0 +1,81 @@
|
||||
# Generated by Django 4.1.5 on 2023-01-04 21:33
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django_extensions.db.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('videos', '0001_initial'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Scrobble',
|
||||
fields=[
|
||||
(
|
||||
'id',
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name='ID',
|
||||
),
|
||||
),
|
||||
(
|
||||
'created',
|
||||
django_extensions.db.fields.CreationDateTimeField(
|
||||
auto_now_add=True, verbose_name='created'
|
||||
),
|
||||
),
|
||||
(
|
||||
'modified',
|
||||
django_extensions.db.fields.ModificationDateTimeField(
|
||||
auto_now=True, verbose_name='modified'
|
||||
),
|
||||
),
|
||||
('timestamp', models.DateTimeField(blank=True, null=True)),
|
||||
(
|
||||
'playback_position_ticks',
|
||||
models.PositiveIntegerField(blank=True, null=True),
|
||||
),
|
||||
(
|
||||
'playback_position',
|
||||
models.CharField(blank=True, max_length=8, null=True),
|
||||
),
|
||||
('is_paused', models.BooleanField(default=False)),
|
||||
('played_to_completion', models.BooleanField(default=False)),
|
||||
(
|
||||
'source',
|
||||
models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
('source_id', models.TextField(blank=True, null=True)),
|
||||
(
|
||||
'user',
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
(
|
||||
'video',
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
to='videos.video',
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'get_latest_by': 'modified',
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
]
|
||||
18
vrobbler/apps/scrobbles/migrations/0002_scrobble_counted.py
Normal file
18
vrobbler/apps/scrobbles/migrations/0002_scrobble_counted.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.1.5 on 2023-01-04 23:11
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('scrobbles', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='scrobble',
|
||||
name='counted',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,22 @@
|
||||
# Generated by Django 4.1.5 on 2023-01-04 23:51
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('scrobbles', '0002_scrobble_counted'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='scrobble',
|
||||
name='counted',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='scrobble',
|
||||
name='in_progress',
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.1.5 on 2023-01-05 17:50
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('scrobbles', '0003_remove_scrobble_counted_scrobble_in_progress'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='scrobble',
|
||||
name='scrobble_log',
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.1.5 on 2023-01-05 19:38
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('scrobbles', '0004_scrobble_scrobble_log'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='scrobble',
|
||||
name='playback_position_ticks',
|
||||
field=models.PositiveBigIntegerField(blank=True, null=True),
|
||||
),
|
||||
]
|
||||
0
vrobbler/apps/scrobbles/migrations/__init__.py
Normal file
0
vrobbler/apps/scrobbles/migrations/__init__.py
Normal file
33
vrobbler/apps/scrobbles/models.py
Normal file
33
vrobbler/apps/scrobbles/models.py
Normal file
@ -0,0 +1,33 @@
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.db import models
|
||||
from django_extensions.db.models import TimeStampedModel
|
||||
|
||||
from videos.models import Video
|
||||
|
||||
User = get_user_model()
|
||||
BNULL = {"blank": True, "null": True}
|
||||
|
||||
|
||||
class Scrobble(TimeStampedModel):
|
||||
video = models.ForeignKey(Video, on_delete=models.DO_NOTHING)
|
||||
user = models.ForeignKey(
|
||||
User, blank=True, null=True, on_delete=models.DO_NOTHING
|
||||
)
|
||||
timestamp = models.DateTimeField(**BNULL)
|
||||
playback_position_ticks = models.PositiveBigIntegerField(**BNULL)
|
||||
playback_position = models.CharField(max_length=8, **BNULL)
|
||||
is_paused = models.BooleanField(default=False)
|
||||
played_to_completion = models.BooleanField(default=False)
|
||||
source = models.CharField(max_length=255, **BNULL)
|
||||
source_id = models.TextField(**BNULL)
|
||||
in_progress = models.BooleanField(default=True)
|
||||
scrobble_log = models.TextField(**BNULL)
|
||||
|
||||
@property
|
||||
def percent_played(self) -> int:
|
||||
return int(
|
||||
(self.playback_position_ticks / self.video.run_time_ticks) * 100
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return f"Scrobble of {self.video} {self.timestamp.year}-{self.timestamp.month}"
|
||||
8
vrobbler/apps/scrobbles/serializers.py
Normal file
8
vrobbler/apps/scrobbles/serializers.py
Normal file
@ -0,0 +1,8 @@
|
||||
from rest_framework import serializers
|
||||
from scrobbles.models import Scrobble
|
||||
|
||||
|
||||
class ScrobbleSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Scrobble
|
||||
fields = "__all__"
|
||||
9
vrobbler/apps/scrobbles/urls.py
Normal file
9
vrobbler/apps/scrobbles/urls.py
Normal file
@ -0,0 +1,9 @@
|
||||
from django.urls import path
|
||||
from scrobbles import views
|
||||
|
||||
app_name = 'scrobbles'
|
||||
|
||||
urlpatterns = [
|
||||
path('', views.scrobble_endpoint, name='scrobble-list'),
|
||||
path('jellyfin/', views.jellyfin_websocket, name='jellyfin-websocket'),
|
||||
]
|
||||
174
vrobbler/apps/scrobbles/views.py
Normal file
174
vrobbler/apps/scrobbles/views.py
Normal file
@ -0,0 +1,174 @@
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from dateutil.parser import parse
|
||||
from django.conf import settings
|
||||
from django.db.models.fields import timezone
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.views.generic.list import ListView
|
||||
from rest_framework import status
|
||||
from rest_framework.decorators import api_view
|
||||
from rest_framework.response import Response
|
||||
|
||||
from scrobbles.models import Scrobble
|
||||
from scrobbles.serializers import ScrobbleSerializer
|
||||
from videos.models import Series, Video
|
||||
from vrobbler.settings import DELETE_STALE_SCROBBLES
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
TRUTHY_VALUES = [
|
||||
'true',
|
||||
'1',
|
||||
't',
|
||||
'y',
|
||||
'yes',
|
||||
'yeah',
|
||||
'yup',
|
||||
'certainly',
|
||||
'uh-huh',
|
||||
]
|
||||
|
||||
|
||||
class RecentScrobbleList(ListView):
|
||||
model = Scrobble
|
||||
|
||||
def get_queryset(self):
|
||||
return Scrobble.objects.filter(in_progress=False).order_by(
|
||||
'-timestamp'
|
||||
)
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
@api_view(['GET'])
|
||||
def scrobble_endpoint(request):
|
||||
"""List all Scrobbles, or create a new Scrobble"""
|
||||
scrobble = Scrobble.objects.all()
|
||||
serializer = ScrobbleSerializer(scrobble, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
@api_view(['POST'])
|
||||
def jellyfin_websocket(request):
|
||||
data_dict = request.data
|
||||
media_type = data_dict["ItemType"]
|
||||
# Check if it's a TV Episode
|
||||
video_dict = {
|
||||
"title": data_dict["Name"],
|
||||
"imdb_id": data_dict["Provider_imdb"],
|
||||
"video_type": Video.VideoType.MOVIE,
|
||||
"year": data_dict["Year"],
|
||||
}
|
||||
if media_type == 'Episode':
|
||||
series_name = data_dict["SeriesName"]
|
||||
series, series_created = Series.objects.get_or_create(name=series_name)
|
||||
|
||||
video_dict['video_type'] = Video.VideoType.TV_EPISODE
|
||||
video_dict["tv_series_id"] = series.id
|
||||
video_dict["episode_number"] = data_dict["EpisodeNumber"]
|
||||
video_dict["season_number"] = data_dict["SeasonNumber"]
|
||||
video_dict["tvdb_id"] = data_dict.get("Provider_tvdb", None)
|
||||
video_dict["tvrage_id"] = data_dict.get("Provider_tvrage", None)
|
||||
|
||||
video, video_created = Video.objects.get_or_create(**video_dict)
|
||||
|
||||
if video_created:
|
||||
video.overview = data_dict["Overview"]
|
||||
video.tagline = data_dict["Tagline"]
|
||||
video.run_time_ticks = data_dict["RunTimeTicks"]
|
||||
video.run_time = data_dict["RunTime"]
|
||||
video.save()
|
||||
|
||||
# Now we run off a scrobble
|
||||
timestamp = parse(data_dict["UtcTimestamp"])
|
||||
scrobble_dict = {
|
||||
'video_id': video.id,
|
||||
'user_id': request.user.id,
|
||||
'in_progress': True,
|
||||
}
|
||||
|
||||
existing_finished_scrobble = (
|
||||
Scrobble.objects.filter(
|
||||
video=video, user_id=request.user.id, in_progress=False
|
||||
)
|
||||
.order_by('-modified')
|
||||
.first()
|
||||
)
|
||||
existing_in_progress_scrobble = (
|
||||
Scrobble.objects.filter(
|
||||
video=video, user_id=request.user.id, in_progress=True
|
||||
)
|
||||
.order_by('-modified')
|
||||
.first()
|
||||
)
|
||||
|
||||
minutes_from_now = timezone.now() + timedelta(minutes=15)
|
||||
a_day_from_now = timezone.now() + timedelta(days=1)
|
||||
|
||||
if (
|
||||
existing_finished_scrobble
|
||||
and existing_finished_scrobble.modified < minutes_from_now
|
||||
):
|
||||
logger.info(
|
||||
'Found a scrobble for this video less than 15 minutes ago, holding off scrobbling again'
|
||||
)
|
||||
return Response(video_dict, status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
existing_scrobble_more_than_a_day_old = (
|
||||
existing_in_progress_scrobble
|
||||
and existing_in_progress_scrobble.modified > a_day_from_now
|
||||
)
|
||||
delete_stale_scrobbles = getattr(settings, "DELETE_STALE_SCROBBLES", True)
|
||||
|
||||
# Check if found in progress scrobble is more than a day old
|
||||
if existing_scrobble_more_than_a_day_old:
|
||||
logger.info(
|
||||
'Found a scrobble for this video more than a day old, creating a new scrobble'
|
||||
)
|
||||
scrobble = existing_in_progress_scrobble
|
||||
scrobble_created = False
|
||||
else:
|
||||
if existing_scrobble_more_than_a_day_old and delete_stale_scrobbles:
|
||||
existing_in_progress_scrobble.delete()
|
||||
scrobble, scrobble_created = Scrobble.objects.get_or_create(
|
||||
**scrobble_dict
|
||||
)
|
||||
|
||||
if scrobble_created:
|
||||
# If we newly created this, capture the client we're watching from
|
||||
scrobble.source = data_dict['ClientName']
|
||||
scrobble.source_id = data_dict['MediaSourceId']
|
||||
scrobble.scrobble_log = ""
|
||||
|
||||
# Update a found scrobble with new position and timestamp
|
||||
scrobble.playback_position_ticks = data_dict["PlaybackPositionTicks"]
|
||||
scrobble.playback_position = data_dict["PlaybackPosition"]
|
||||
scrobble.timestamp = parse(data_dict["UtcTimestamp"])
|
||||
scrobble.is_paused = data_dict["IsPaused"] in TRUTHY_VALUES
|
||||
scrobble.save(
|
||||
update_fields=[
|
||||
'playback_position_ticks',
|
||||
'playback_position',
|
||||
'timestamp',
|
||||
'is_paused',
|
||||
]
|
||||
)
|
||||
|
||||
# If we hit our completion threshold, save it and get ready
|
||||
# to scrobble again if we re-watch this.
|
||||
if scrobble.percent_played >= getattr(
|
||||
settings, "PERCENT_FOR_COMPLETION", 95
|
||||
):
|
||||
scrobble.in_progress = False
|
||||
scrobble.playback_position_ticks = video.run_time_ticks
|
||||
scrobble.save()
|
||||
|
||||
if scrobble.percent_played % 5 == 0:
|
||||
if getattr(settings, "KEEP_DETAILED_SCROBBLE_LOGS", False):
|
||||
scrobble.scrobble_log += f"\n{str(scrobble.timestamp)} - {scrobble.playback_position} - {str(scrobble.playback_position_ticks)} - {str(scrobble.percent_played)}%"
|
||||
scrobble.save(update_fields=['scrobble_log'])
|
||||
logger.debug(f"You are {scrobble.percent_played}% through {video}")
|
||||
|
||||
return Response(video_dict, status=status.HTTP_201_CREATED)
|
||||
0
vrobbler/apps/videos/__init__.py
Normal file
0
vrobbler/apps/videos/__init__.py
Normal file
28
vrobbler/apps/videos/admin.py
Normal file
28
vrobbler/apps/videos/admin.py
Normal file
@ -0,0 +1,28 @@
|
||||
from django.contrib import admin
|
||||
|
||||
from videos.models import Series, Video
|
||||
|
||||
|
||||
class SeriesAdmin(admin.ModelAdmin):
|
||||
date_hierarchy = "created"
|
||||
list_display = ("name", "tagline")
|
||||
ordering = ("-created",)
|
||||
|
||||
|
||||
class VideoAdmin(admin.ModelAdmin):
|
||||
date_hierarchy = "created"
|
||||
list_display = (
|
||||
"title",
|
||||
"video_type",
|
||||
"year",
|
||||
"tv_series",
|
||||
"season_number",
|
||||
"episode_number",
|
||||
"imdb_id",
|
||||
)
|
||||
list_filter = ("year", "tv_series", "video_type")
|
||||
ordering = ("-created",)
|
||||
|
||||
|
||||
admin.site.register(Series, SeriesAdmin)
|
||||
admin.site.register(Video, VideoAdmin)
|
||||
5
vrobbler/apps/videos/apps.py
Normal file
5
vrobbler/apps/videos/apps.py
Normal file
@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class VideosConfig(AppConfig):
|
||||
name = 'videos'
|
||||
128
vrobbler/apps/videos/migrations/0001_initial.py
Normal file
128
vrobbler/apps/videos/migrations/0001_initial.py
Normal file
@ -0,0 +1,128 @@
|
||||
# Generated by Django 4.1.5 on 2023-01-04 21:33
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django_extensions.db.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = []
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Series',
|
||||
fields=[
|
||||
(
|
||||
'id',
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name='ID',
|
||||
),
|
||||
),
|
||||
(
|
||||
'created',
|
||||
django_extensions.db.fields.CreationDateTimeField(
|
||||
auto_now_add=True, verbose_name='created'
|
||||
),
|
||||
),
|
||||
(
|
||||
'modified',
|
||||
django_extensions.db.fields.ModificationDateTimeField(
|
||||
auto_now=True, verbose_name='modified'
|
||||
),
|
||||
),
|
||||
('name', models.CharField(max_length=255)),
|
||||
('overview', models.TextField(blank=True, null=True)),
|
||||
('tagline', models.TextField(blank=True, null=True)),
|
||||
],
|
||||
options={
|
||||
'get_latest_by': 'modified',
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Video',
|
||||
fields=[
|
||||
(
|
||||
'id',
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name='ID',
|
||||
),
|
||||
),
|
||||
(
|
||||
'created',
|
||||
django_extensions.db.fields.CreationDateTimeField(
|
||||
auto_now_add=True, verbose_name='created'
|
||||
),
|
||||
),
|
||||
(
|
||||
'modified',
|
||||
django_extensions.db.fields.ModificationDateTimeField(
|
||||
auto_now=True, verbose_name='modified'
|
||||
),
|
||||
),
|
||||
(
|
||||
'video_type',
|
||||
models.CharField(
|
||||
choices=[
|
||||
('U', 'Unknown'),
|
||||
('E', 'TV Episode'),
|
||||
('M', 'Movie'),
|
||||
],
|
||||
default='U',
|
||||
max_length=1,
|
||||
),
|
||||
),
|
||||
(
|
||||
'title',
|
||||
models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
('overview', models.TextField(blank=True, null=True)),
|
||||
('tagline', models.TextField(blank=True, null=True)),
|
||||
(
|
||||
'run_time',
|
||||
models.CharField(blank=True, max_length=8, null=True),
|
||||
),
|
||||
(
|
||||
'run_time_ticks',
|
||||
models.BigIntegerField(blank=True, null=True),
|
||||
),
|
||||
('year', models.IntegerField()),
|
||||
('season_number', models.IntegerField(blank=True, null=True)),
|
||||
('episode_number', models.IntegerField(blank=True, null=True)),
|
||||
(
|
||||
'tvdb_id',
|
||||
models.CharField(blank=True, max_length=20, null=True),
|
||||
),
|
||||
(
|
||||
'imdb_id',
|
||||
models.CharField(blank=True, max_length=20, null=True),
|
||||
),
|
||||
(
|
||||
'tvrage_id',
|
||||
models.CharField(blank=True, max_length=20, null=True),
|
||||
),
|
||||
(
|
||||
'tv_series',
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
to='videos.series',
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'get_latest_by': 'modified',
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
]
|
||||
17
vrobbler/apps/videos/migrations/0002_alter_series_options.py
Normal file
17
vrobbler/apps/videos/migrations/0002_alter_series_options.py
Normal file
@ -0,0 +1,17 @@
|
||||
# Generated by Django 4.1.5 on 2023-01-05 16:49
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('videos', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='series',
|
||||
options={'verbose_name_plural': 'series'},
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.1.5 on 2023-01-05 19:38
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('videos', '0002_alter_series_options'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='video',
|
||||
name='run_time_ticks',
|
||||
field=models.PositiveBigIntegerField(blank=True, null=True),
|
||||
),
|
||||
]
|
||||
0
vrobbler/apps/videos/migrations/__init__.py
Normal file
0
vrobbler/apps/videos/migrations/__init__.py
Normal file
52
vrobbler/apps/videos/models.py
Normal file
52
vrobbler/apps/videos/models.py
Normal file
@ -0,0 +1,52 @@
|
||||
from django.db import models
|
||||
from django_extensions.db.models import TimeStampedModel
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
BNULL = {"blank": True, "null": True}
|
||||
|
||||
|
||||
class Series(TimeStampedModel):
|
||||
name = models.CharField(max_length=255)
|
||||
overview = models.TextField(**BNULL)
|
||||
tagline = models.TextField(**BNULL)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class Meta:
|
||||
verbose_name_plural = "series"
|
||||
|
||||
|
||||
class Video(TimeStampedModel):
|
||||
class VideoType(models.TextChoices):
|
||||
UNKNOWN = 'U', _('Unknown')
|
||||
TV_EPISODE = 'E', _('TV Episode')
|
||||
MOVIE = 'M', _('Movie')
|
||||
|
||||
# General fields
|
||||
video_type = models.CharField(
|
||||
max_length=1,
|
||||
choices=VideoType.choices,
|
||||
default=VideoType.UNKNOWN,
|
||||
)
|
||||
title = models.CharField(max_length=255, **BNULL)
|
||||
overview = models.TextField(**BNULL)
|
||||
tagline = models.TextField(**BNULL)
|
||||
run_time = models.CharField(max_length=8, **BNULL)
|
||||
run_time_ticks = models.PositiveBigIntegerField(**BNULL)
|
||||
year = models.IntegerField()
|
||||
|
||||
# TV show specific fields
|
||||
tv_series = models.ForeignKey(Series, on_delete=models.DO_NOTHING, **BNULL)
|
||||
season_number = models.IntegerField(**BNULL)
|
||||
episode_number = models.IntegerField(**BNULL)
|
||||
tvdb_id = models.CharField(max_length=20, **BNULL)
|
||||
imdb_id = models.CharField(max_length=20, **BNULL)
|
||||
tvrage_id = models.CharField(max_length=20, **BNULL)
|
||||
|
||||
# Metadata fields from TMDB
|
||||
|
||||
def __str__(self):
|
||||
if self.video_type == self.VideoType.TV_EPISODE:
|
||||
return f"{self.tv_series} - Season {self.season_number}, Episode {self.episode_number}"
|
||||
return self.title
|
||||
@ -4,7 +4,12 @@ import sys
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from dotenv import load_dotenv
|
||||
from pathlib import Path
|
||||
|
||||
# PROJECT_ROOT = os.path.dirname(__file__)
|
||||
PROJECT_ROOT = Path(__file__).resolve().parent
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, os.path.join(PROJECT_ROOT, 'apps'))
|
||||
|
||||
# Tap vrobbler.conf if it's available
|
||||
if os.path.exists("vrobbler.conf"):
|
||||
@ -14,10 +19,7 @@ elif os.path.exists("/etc/vrobbler.conf"):
|
||||
elif os.path.exists("/usr/local/etc/vrobbler.conf"):
|
||||
load_dotenv("/usr/local/etc/vrobbler.conf")
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
@ -94,7 +96,7 @@ ROOT_URLCONF = "vrobbler.urls"
|
||||
TEMPLATES = [
|
||||
{
|
||||
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||
"DIRS": [str(BASE_DIR.joinpath("templates"))], # new
|
||||
"DIRS": [str(PROJECT_ROOT.joinpath("templates"))],
|
||||
"APP_DIRS": True,
|
||||
"OPTIONS": {
|
||||
"context_processors": [
|
||||
@ -111,7 +113,7 @@ WSGI_APPLICATION = "vrobbler.wsgi.application"
|
||||
|
||||
DATABASES = {
|
||||
"default": dj_database_url.config(
|
||||
default=os.getenv("vrobbler_DATABASE_URL", "sqlite:///db.sqlite3"),
|
||||
default=os.getenv("VROBBLER_DATABASE_URL", "sqlite:///db.sqlite3"),
|
||||
conn_max_age=600,
|
||||
),
|
||||
}
|
||||
@ -181,7 +183,7 @@ AUTH_PASSWORD_VALIDATORS = [
|
||||
|
||||
LANGUAGE_CODE = "en-us"
|
||||
|
||||
TIME_ZONE = os.getenv("vrobbler_TIME_ZONE", "EST")
|
||||
TIME_ZONE = os.getenv("VROBBLER_TIME_ZONE", "EST")
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
@ -195,7 +197,7 @@ USE_TZ = True
|
||||
|
||||
STATIC_URL = "static/"
|
||||
STATIC_ROOT = os.getenv(
|
||||
"VROBBLER_STATIC_ROOT", os.path.join(BASE_DIR, "static")
|
||||
"VROBBLER_STATIC_ROOT", os.path.join(PROJECT_ROOT, "static")
|
||||
)
|
||||
if not DEBUG:
|
||||
STATICFILES_STORAGE = (
|
||||
|
||||
128
vrobbler/templates/base.html
Normal file
128
vrobbler/templates/base.html
Normal file
@ -0,0 +1,128 @@
|
||||
{% load static %}
|
||||
<!doctype html>
|
||||
<html class="no-js" lang="">
|
||||
<head>
|
||||
<title>{% block page_title %}Welcome{% endblock %} | Vrobbler » For video scrobbling</title>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||
<meta name="description" content="">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.0.0/dist/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
|
||||
<script src="https://code.jquery.com/jquery-3.3.1.js" integrity="sha256-2Kok7MbOyxpgUVvAk/HJ2jigOSYS2auK4Pfzbm7uH60=" crossorigin="anonymous"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.0.0/dist/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/2.0.10/clipboard.min.js"></script>
|
||||
|
||||
<style type="text/css">
|
||||
dl {
|
||||
display: flex;
|
||||
flex-flow: column wrap;
|
||||
max-height: 6em;
|
||||
border: 1px solid #777;
|
||||
}
|
||||
dt {
|
||||
padding: 2px 4px;
|
||||
background: #777;
|
||||
color: #fff;
|
||||
}
|
||||
dd {
|
||||
margin: 0;
|
||||
padding: 4px;
|
||||
min-height: 3em;
|
||||
border-right: 1px solid #777;
|
||||
}
|
||||
#library-update-status { margin-right:10px; }
|
||||
.card img { width:18em; padding: 1em; }
|
||||
.card-block { padding: 1em 0 1em 0; }
|
||||
.system-badge { padding: 1em; font-size: normal; }
|
||||
.updating { color:#aaa; margin-right: 8px; }
|
||||
.high { color: green; }
|
||||
.medium { color: #aaa;}
|
||||
.low { color: red; }
|
||||
.card {
|
||||
height: auto;
|
||||
display: flex;
|
||||
}
|
||||
.card-columns{
|
||||
display: grid;
|
||||
overflow: hidden;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
grid-auto-rows: 1fr;
|
||||
grid-column-gap: 20px;
|
||||
grid-row-gap: 10px;
|
||||
}
|
||||
{% for system in game_systems %}
|
||||
.{{system.retropie_slug}} { background: #{{system.get_color}}; }
|
||||
{% endfor %}
|
||||
</style>
|
||||
{% block head_extra %}{% endblock %}
|
||||
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
|
||||
<!-- Place favicon.ico in the root directory -->
|
||||
|
||||
<script>
|
||||
function checkUpdate(){
|
||||
$.get('/library/update/status/', function(data) {
|
||||
$('#library-update-status').html("");
|
||||
console.log('Checking for task');
|
||||
setTimeout(checkUpdate,5000);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<!--[if lt IE 8]>
|
||||
<p class="browserupgrade">
|
||||
You are using an <strong>outdated</strong> browser. Please
|
||||
<a href="http://browsehappy.com/">upgrade your browser</a> to improve
|
||||
your experience.
|
||||
</p>
|
||||
<![endif]-->
|
||||
<div class="container">
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-light">
|
||||
<a class="navbar-brand" href="#">Vrobbler</a>
|
||||
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||
<ul class="navbar-nav mr-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'home' %}">Recent<span class="sr-only"></span></a>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">Movies</a>
|
||||
<div class="dropdown-menu" aria-labelledby="navbarDropdown">
|
||||
<a class="dropdown-item" href="{ url "games:gamesystem_list" %}">All</a>
|
||||
{% for system in game_systems %}
|
||||
<a class="dropdown-item" href="{{system.get_absolute_url}}">{{system.name}} ({{system.game_set.count}})</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">Shows</a>
|
||||
<div class="dropdown-menu" aria-labelledby="navbarDropdown">
|
||||
<a class="dropdown-item" href="{ url "games:gamecollection_list" %}">All</a>
|
||||
{% for collection in game_collections %}
|
||||
<a class="dropdown-item" href="{{collection.get_absolute_url}}">{{collection.name}} ({{collection.games.count}})</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{% if request.user.is_authenticated %}
|
||||
<a class="nav-link" href="{% url 'account_logout' %}">Logout<span class="sr-only"></span></a>
|
||||
{% else %}
|
||||
<a class="nav-link" href="{% url 'account_login' %}">Login<span class="sr-only"></span></a>
|
||||
{% endif %}
|
||||
</nav>
|
||||
|
||||
<h1>{% block title %}{% endblock %}</h1>
|
||||
|
||||
<div>
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
11
vrobbler/templates/scrobbles/scrobble_list.html
Normal file
11
vrobbler/templates/scrobbles/scrobble_list.html
Normal file
@ -0,0 +1,11 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Last watched{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<ul>
|
||||
{% for scrobble in object_list %}
|
||||
<li>{{scrobble.timestamp|date:"D, M j Y"}}: <a href="https://www.imdb.com/title/{{scrobble.video.imdb_id}}">{{scrobble.video}}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user