Initial commit
This commit is contained in:
32
.drone.yml
Normal file
32
.drone.yml
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
---
|
||||||
|
################
|
||||||
|
# Build & Test #
|
||||||
|
################
|
||||||
|
|
||||||
|
kind: pipeline
|
||||||
|
name: run_tests
|
||||||
|
|
||||||
|
steps:
|
||||||
|
# Run tests against Python/Flask engine backend (with pytest)
|
||||||
|
- name: django_tests
|
||||||
|
image: python:3.10.4
|
||||||
|
commands:
|
||||||
|
# Install dependencies
|
||||||
|
- cp vrobbler.conf.example vrobbler.conf
|
||||||
|
- pip install poetry
|
||||||
|
- poetry install
|
||||||
|
# Start with a fresh database (which is already running as a service from Drone)
|
||||||
|
- poetry run python manage.py test
|
||||||
|
environment:
|
||||||
|
VROBBLER_DATABASE_URL: sqlite:///test.db
|
||||||
|
volumes:
|
||||||
|
# Mount pip cache from host
|
||||||
|
- name: pip_cache
|
||||||
|
path: /root/.cache/pip
|
||||||
|
volumes:
|
||||||
|
- name: docker
|
||||||
|
host:
|
||||||
|
path: /var/run/docker.sock
|
||||||
|
- name: pip_cache
|
||||||
|
host:
|
||||||
|
path: /tmp/cache/drone/pip
|
||||||
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
db.sqlite3
|
||||||
|
vrobbler.conf
|
||||||
|
/media/
|
||||||
22
manage.py
Executable file
22
manage.py
Executable file
@ -0,0 +1,22 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
"""Django's command-line utility for administrative tasks."""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Run administrative tasks."""
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'vrobbler.settings')
|
||||||
|
try:
|
||||||
|
from django.core.management import execute_from_command_line
|
||||||
|
except ImportError as exc:
|
||||||
|
raise ImportError(
|
||||||
|
"Couldn't import Django. Are you sure it's installed and "
|
||||||
|
"available on your PYTHONPATH environment variable? Did you "
|
||||||
|
"forget to activate a virtual environment?"
|
||||||
|
) from exc
|
||||||
|
execute_from_command_line(sys.argv)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
2101
poetry.lock
generated
Normal file
2101
poetry.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
70
pyproject.toml
Normal file
70
pyproject.toml
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
[tool.poetry]
|
||||||
|
name = "vrobbler"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = ""
|
||||||
|
authors = ["Colin Powell <colin@unbl.ink>"]
|
||||||
|
|
||||||
|
[tool.poetry.dependencies]
|
||||||
|
python = "^3.8"
|
||||||
|
Django = "^4.0.3"
|
||||||
|
django-extensions = "^3.1.5"
|
||||||
|
python-dateutil = "^2.8.2"
|
||||||
|
python-dotenv = "^0.20.0"
|
||||||
|
python-json-logger = "^2.0.2"
|
||||||
|
colorlog = "^6.6.0"
|
||||||
|
djangorestframework = "^3.13.1"
|
||||||
|
Markdown = "^3.3.6"
|
||||||
|
django-filter = "^21.1"
|
||||||
|
Pillow = "^9.0.1"
|
||||||
|
psycopg2 = {version = "^2.9.3", extras = ["production"]}
|
||||||
|
dj-database-url = "^0.5.0"
|
||||||
|
django-mathfilters = "^1.0.0"
|
||||||
|
django-allauth = "^0.50.0"
|
||||||
|
django-celery-results = "^2.3.0"
|
||||||
|
redis = "^4.2.2"
|
||||||
|
django-taggit = "^2.1.0"
|
||||||
|
django-markdownify = "^0.9.1"
|
||||||
|
gunicorn = "^20.1.0"
|
||||||
|
django-simple-history = "^3.1.1"
|
||||||
|
|
||||||
|
[tool.poetry.dev-dependencies]
|
||||||
|
Werkzeug = "2.0.3"
|
||||||
|
black = "^22.3"
|
||||||
|
freezegun = "^1.2"
|
||||||
|
mypy = "^0.961"
|
||||||
|
pytest = "^7.1"
|
||||||
|
pytest-black = "^0.3.12"
|
||||||
|
pytest-cov = "^3.0"
|
||||||
|
pytest-flake8 = "^1.1"
|
||||||
|
pytest-isort = "^3.0"
|
||||||
|
pytest-runner = "^6.0"
|
||||||
|
pytest-selenium = "^2.0.1"
|
||||||
|
types-pytz = "^2022.1"
|
||||||
|
types-requests = "^2.27"
|
||||||
|
types-freezegun = "^1.1"
|
||||||
|
bandit = "^1.7.4"
|
||||||
|
|
||||||
|
[tool.black]
|
||||||
|
line-length = 79
|
||||||
|
skip-string-normalization = true
|
||||||
|
target-version = ["py39", "py310"]
|
||||||
|
include = ".py$"
|
||||||
|
exclude = "migrations"
|
||||||
|
|
||||||
|
[tool.isort]
|
||||||
|
multi_line_output = 3
|
||||||
|
include_trailing_comma = true
|
||||||
|
force_grid_wrap = 0
|
||||||
|
combine_as_imports = true
|
||||||
|
|
||||||
|
[tool.bandit]
|
||||||
|
exclude_dirs = ["*/tests/*", "*/migrations/*"]
|
||||||
|
|
||||||
|
[tool.poetry.scripts]
|
||||||
|
server = 'scripts:server'
|
||||||
|
migrate = 'scripts:migrate'
|
||||||
|
shell = 'scripts:shell'
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["poetry-core>=1.0.0"]
|
||||||
|
build-backend = "poetry.core.masonry.api"
|
||||||
13
scripts.py
Normal file
13
scripts.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import subprocess
|
||||||
|
|
||||||
|
def server():
|
||||||
|
cmd =['python', 'manage.py', 'runserver_plus']
|
||||||
|
subprocess.run(cmd)
|
||||||
|
|
||||||
|
def migrate():
|
||||||
|
cmd =['python', 'manage.py', 'migrate']
|
||||||
|
subprocess.run(cmd)
|
||||||
|
|
||||||
|
def shell():
|
||||||
|
cmd =['python', 'manage.py', 'shell_plus']
|
||||||
|
subprocess.run(cmd)
|
||||||
0
scrobbles/__init__.py
Normal file
0
scrobbles/__init__.py
Normal file
13
scrobbles/admin.py
Normal file
13
scrobbles/admin.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
from scrobbles.models import Scrobble
|
||||||
|
|
||||||
|
|
||||||
|
class ScrobbleAdmin(admin.ModelAdmin):
|
||||||
|
date_hierarchy = "timestamp"
|
||||||
|
list_display = ("video", "timestamp", "source", "playback_position")
|
||||||
|
list_filter = ("video",)
|
||||||
|
ordering = ("-timestamp",)
|
||||||
|
|
||||||
|
|
||||||
|
admin.site.register(Scrobble, ScrobbleAdmin)
|
||||||
5
scrobbles/apps.py
Normal file
5
scrobbles/apps.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class ScrobblesConfig(AppConfig):
|
||||||
|
name = 'scrobbles'
|
||||||
81
scrobbles/migrations/0001_initial.py
Normal file
81
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,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
0
scrobbles/migrations/__init__.py
Normal file
0
scrobbles/migrations/__init__.py
Normal file
25
scrobbles/models.py
Normal file
25
scrobbles/models.py
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
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.PositiveIntegerField(**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)
|
||||||
|
|
||||||
|
def percent_played(self):
|
||||||
|
return int((self.playback_position_ticks / video.run_time_ticks) * 100)
|
||||||
8
scrobbles/serializers.py
Normal file
8
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__"
|
||||||
25
scrobbles/templates/scrobbles/scrobble_list.html
Normal file
25
scrobbles/templates/scrobbles/scrobble_list.html
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html class="no-js" lang="">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||||
|
<title>Untitled</title>
|
||||||
|
<meta name="description" content="">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
|
||||||
|
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
|
||||||
|
<!-- Place favicon.ico in the root directory -->
|
||||||
|
|
||||||
|
</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]-->
|
||||||
|
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
8
scrobbles/urls.py
Normal file
8
scrobbles/urls.py
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
from django.urls import path
|
||||||
|
from scrobbles import views
|
||||||
|
|
||||||
|
app_name = 'scrobbles'
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('', views.scrobble_list, name='scrobble-list'),
|
||||||
|
]
|
||||||
98
scrobbles/views.py
Normal file
98
scrobbles/views.py
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import dateutil
|
||||||
|
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
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
TRUTHY_VALUES = [
|
||||||
|
'true',
|
||||||
|
'1',
|
||||||
|
't',
|
||||||
|
'y',
|
||||||
|
'yes',
|
||||||
|
'yeah',
|
||||||
|
'yup',
|
||||||
|
'certainly',
|
||||||
|
'uh-huh',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class RecentScrobbleList(ListView):
|
||||||
|
model = Scrobble
|
||||||
|
|
||||||
|
|
||||||
|
@csrf_exempt
|
||||||
|
@api_view(['GET', 'POST'])
|
||||||
|
def scrobble_list(request):
|
||||||
|
"""List all Scrobbles, or create a new Scrobble"""
|
||||||
|
if request.method == 'GET':
|
||||||
|
scrobble = Scrobble.objects.all()
|
||||||
|
serializer = ScrobbleSerializer(scrobble, many=True)
|
||||||
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
elif request.method == 'POST':
|
||||||
|
data_dict = json.loads(request.data["_content"])
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
if media_type == 'Episode':
|
||||||
|
series_name = data_dict["SeriesName"]
|
||||||
|
series = 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_num"] = data_dict["EpisodeNumber"]
|
||||||
|
video_dict["season_num"] = data_dict["SeasonNumber"]
|
||||||
|
video_dict["tvdb_id"] = data_dict["Provider_tvdb"]
|
||||||
|
video_dict["tvrage_id"] = data_dict["Provider_tvrage"]
|
||||||
|
|
||||||
|
video, _created = Video.objects.get_or_create(**video_dict)
|
||||||
|
|
||||||
|
video.year = data_dict["Year"]
|
||||||
|
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
|
||||||
|
scrobble_dict = {
|
||||||
|
'video_id': video.id,
|
||||||
|
'user_id': request.user.id,
|
||||||
|
}
|
||||||
|
scrobble, scrobble_created = Scrobble.objects.get_or_create(
|
||||||
|
**scrobble_dict
|
||||||
|
)
|
||||||
|
|
||||||
|
if scrobble_created:
|
||||||
|
scrobble.source = data_dict['ClientName']
|
||||||
|
scrobble.source_id = data_dict['MediaSourceId']
|
||||||
|
|
||||||
|
# Update a found scrobble with new position and timestamp
|
||||||
|
scrobble.playback_position_ticks = data_dict["PlaybackPositionTicks"]
|
||||||
|
scrobble.playback_position = data_dict["PlaybackPosition"]
|
||||||
|
scrobble.timestamp = dateutil.parser.parse(data_dict["UtcTimestamp"])
|
||||||
|
scrobble.is_paused = data_dict["IsPaused"] in TRUTHY_VALUES
|
||||||
|
scrobble.played_to_completion = (
|
||||||
|
data_dict["PlayedToCompletion"] in TRUTHY_VALUES
|
||||||
|
)
|
||||||
|
scrobble.save()
|
||||||
|
|
||||||
|
logger.info(f"You are {scrobble.percent_played}% through {video}")
|
||||||
|
|
||||||
|
return Response(video_dict, status=status.HTTP_201_CREATED)
|
||||||
|
# return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
136
templates/base.html
Normal file
136
templates/base.html
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
{% 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>
|
||||||
|
|
||||||
|
{% if request.user.is_authenticated %}
|
||||||
|
<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 ">
|
||||||
|
<a class="nav-link" href="{% url 'games:game_list' %}">Library<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">Systems</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">Collections</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>
|
||||||
|
{% if update_in_progress %}<em class="updating">Updating</em><img id="library-update-status" src="{% static 'img/spinner.gif'%}"" width=30 />{% endif %}
|
||||||
|
<form class="form-inline my-2 my-lg-0" method="get" action="{% url 'search:search' %}">
|
||||||
|
<input class="form-control mr-sm-2" name="q" type="search" placeholder="Search" aria-label="Search">
|
||||||
|
<button class="btn btn-primary my-2 my-sm-0" type="submit">Search</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<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>
|
||||||
0
videos/__init__.py
Normal file
0
videos/__init__.py
Normal file
28
videos/admin.py
Normal file
28
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")
|
||||||
|
ordering = ("-created",)
|
||||||
|
|
||||||
|
|
||||||
|
admin.site.register(Series, SeriesAdmin)
|
||||||
|
admin.site.register(Video, VideoAdmin)
|
||||||
5
videos/apps.py
Normal file
5
videos/apps.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class VideosConfig(AppConfig):
|
||||||
|
name = 'videos'
|
||||||
128
videos/migrations/0001_initial.py
Normal file
128
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,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
0
videos/migrations/__init__.py
Normal file
0
videos/migrations/__init__.py
Normal file
39
videos/models.py
Normal file
39
videos/models.py
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
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.BigIntegerField(**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)
|
||||||
5
vrobbler.conf.example
Normal file
5
vrobbler.conf.example
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# You can use this file to set environment variables for your local setup
|
||||||
|
#
|
||||||
|
VROBBLER_MEDIA_ROOT="/media"
|
||||||
|
VROBBLER_DATABASE_URL="postgres://USER:PASSWORD@HOST:PORT/NAME"
|
||||||
|
VROBBLER_JSON_LOGGING=True
|
||||||
0
vrobbler/__init__.py
Normal file
0
vrobbler/__init__.py
Normal file
16
vrobbler/asgi.py
Normal file
16
vrobbler/asgi.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
"""
|
||||||
|
ASGI config for vrobbler project.
|
||||||
|
|
||||||
|
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/3.1/howto/deployment/asgi/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.asgi import get_asgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'vrobbler.settings')
|
||||||
|
|
||||||
|
application = get_asgi_application()
|
||||||
278
vrobbler/settings.py
Normal file
278
vrobbler/settings.py
Normal file
@ -0,0 +1,278 @@
|
|||||||
|
import dj_database_url
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
|
||||||
|
# Tap vrobbler.conf if it's available
|
||||||
|
if os.path.exists("vrobbler.conf"):
|
||||||
|
load_dotenv("vrobbler.conf")
|
||||||
|
elif os.path.exists("/etc/vrobbler.conf"):
|
||||||
|
load_dotenv("/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
|
||||||
|
# See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/
|
||||||
|
|
||||||
|
# SECURITY WARNING: keep the secret key used in production secret!
|
||||||
|
SECRET_KEY = os.getenv("VROBBLER_SECRET_KEY", "not-a-secret-234lkjasdflj132")
|
||||||
|
|
||||||
|
|
||||||
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
|
DEBUG = os.getenv("VROBBLER_DEBUG", False)
|
||||||
|
|
||||||
|
TESTING = len(sys.argv) > 1 and sys.argv[1] == "test"
|
||||||
|
|
||||||
|
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||||
|
|
||||||
|
ALLOWED_HOSTS = ["*"]
|
||||||
|
CSRF_TRUSTED_ORIGINS = [
|
||||||
|
os.getenv("vrobbler_TRUSTED_ORIGINS", "http://localhost:8000")
|
||||||
|
]
|
||||||
|
X_FRAME_OPTIONS = "SAMEORIGIN"
|
||||||
|
|
||||||
|
REDIS_URL = os.getenv("VROBBLER_REDIS_URL", None)
|
||||||
|
|
||||||
|
CELERY_TASK_ALWAYS_EAGER = os.getenv("VROBBLER_SKIP_CELERY", False)
|
||||||
|
CELERY_BROKER_URL = REDIS_URL if REDIS_URL else "memory://localhost/"
|
||||||
|
CELERY_RESULT_BACKEND = "django-db"
|
||||||
|
CELERY_TIMEZONE = os.getenv("VROBBLER_TIME_ZONE", "EST")
|
||||||
|
CELERY_TASK_TRACK_STARTED = True
|
||||||
|
|
||||||
|
INSTALLED_APPS = [
|
||||||
|
"django.contrib.admin",
|
||||||
|
"django.contrib.auth",
|
||||||
|
"django.contrib.contenttypes",
|
||||||
|
"django.contrib.sessions",
|
||||||
|
"django.contrib.messages",
|
||||||
|
"django.contrib.staticfiles",
|
||||||
|
"django.contrib.sites",
|
||||||
|
"django_filters",
|
||||||
|
"django_extensions",
|
||||||
|
'rest_framework.authtoken',
|
||||||
|
"vrobbler",
|
||||||
|
"scrobbles",
|
||||||
|
"videos",
|
||||||
|
"rest_framework",
|
||||||
|
"allauth",
|
||||||
|
"allauth.account",
|
||||||
|
"django_celery_results",
|
||||||
|
]
|
||||||
|
|
||||||
|
SITE_ID = 1
|
||||||
|
|
||||||
|
MIDDLEWARE = [
|
||||||
|
"django.middleware.security.SecurityMiddleware",
|
||||||
|
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||||
|
"django.middleware.common.CommonMiddleware",
|
||||||
|
"django.middleware.csrf.CsrfViewMiddleware",
|
||||||
|
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||||
|
"django.contrib.messages.middleware.MessageMiddleware",
|
||||||
|
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||||
|
"django.middleware.gzip.GZipMiddleware",
|
||||||
|
]
|
||||||
|
|
||||||
|
ROOT_URLCONF = "vrobbler.urls"
|
||||||
|
|
||||||
|
TEMPLATES = [
|
||||||
|
{
|
||||||
|
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||||
|
"DIRS": [str(BASE_DIR.joinpath("templates"))], # new
|
||||||
|
"APP_DIRS": True,
|
||||||
|
"OPTIONS": {
|
||||||
|
"context_processors": [
|
||||||
|
"django.template.context_processors.debug",
|
||||||
|
"django.template.context_processors.request",
|
||||||
|
"django.contrib.auth.context_processors.auth",
|
||||||
|
"django.contrib.messages.context_processors.messages",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
WSGI_APPLICATION = "vrobbler.wsgi.application"
|
||||||
|
|
||||||
|
DATABASES = {
|
||||||
|
"default": dj_database_url.config(
|
||||||
|
default=os.getenv("vrobbler_DATABASE_URL", "sqlite:///db.sqlite3"),
|
||||||
|
conn_max_age=600,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
if TESTING:
|
||||||
|
DATABASES = {
|
||||||
|
"default": dj_database_url.config(default="sqlite:///testdb.sqlite3")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
CACHES = {
|
||||||
|
"default": {
|
||||||
|
"BACKEND": "django.core.cache.backends.locmem.LocMemCache",
|
||||||
|
"LOCATION": "unique-snowflake",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if REDIS_URL:
|
||||||
|
CACHES["default"][
|
||||||
|
"BACKEND"
|
||||||
|
] = "django.core.cache.backends.redis.RedisCache"
|
||||||
|
CACHES["default"]["LOCATION"] = REDIS_URL
|
||||||
|
|
||||||
|
SESSION_ENGINE = "django.contrib.sessions.backends.cached_db"
|
||||||
|
|
||||||
|
AUTHENTICATION_BACKENDS = [
|
||||||
|
"django.contrib.auth.backends.ModelBackend",
|
||||||
|
"allauth.account.auth_backends.AuthenticationBackend",
|
||||||
|
]
|
||||||
|
|
||||||
|
REST_FRAMEWORK = {
|
||||||
|
"DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.AllowAny",),
|
||||||
|
'DEFAULT_AUTHENTICATION_CLASSES': [
|
||||||
|
'rest_framework.authentication.BasicAuthentication',
|
||||||
|
'rest_framework.authentication.TokenAuthentication',
|
||||||
|
'rest_framework.authentication.SessionAuthentication',
|
||||||
|
],
|
||||||
|
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
|
||||||
|
"DEFAULT_FILTER_BACKENDS": [
|
||||||
|
"django_filters.rest_framework.DjangoFilterBackend"
|
||||||
|
],
|
||||||
|
"PAGE_SIZE": 100,
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGIN_REDIRECT_URL = "/"
|
||||||
|
|
||||||
|
AUTH_PASSWORD_VALIDATORS = [
|
||||||
|
{
|
||||||
|
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# Internationalization
|
||||||
|
# https://docs.djangoproject.com/en/3.1/topics/i18n/
|
||||||
|
|
||||||
|
LANGUAGE_CODE = "en-us"
|
||||||
|
|
||||||
|
TIME_ZONE = os.getenv("vrobbler_TIME_ZONE", "EST")
|
||||||
|
|
||||||
|
USE_I18N = True
|
||||||
|
|
||||||
|
USE_L10N = True
|
||||||
|
|
||||||
|
USE_TZ = True
|
||||||
|
|
||||||
|
|
||||||
|
# Static files (CSS, JavaScript, Images)
|
||||||
|
# https://docs.djangoproject.com/en/3.1/howto/static-files/
|
||||||
|
|
||||||
|
STATIC_URL = "static/"
|
||||||
|
STATIC_ROOT = os.getenv(
|
||||||
|
"vrobbler_STATIC_ROOT", os.path.join(BASE_DIR, "static")
|
||||||
|
)
|
||||||
|
|
||||||
|
MEDIA_URL = "/media/"
|
||||||
|
MEDIA_ROOT = os.getenv("vrobbler_MEDIA_ROOT", os.path.join(BASE_DIR, "media"))
|
||||||
|
ROMS_DIR = os.path.join(MEDIA_ROOT, "roms")
|
||||||
|
COLLECTIONS_DIR = os.path.join(ROMS_DIR, "emulationstation-collections")
|
||||||
|
|
||||||
|
SCRAPER_BIN_PATH = os.getenv("vrobbler_SCRAPER_BINPATH", "Skyscraper")
|
||||||
|
SCRAPER_CONFIG_FILE = os.getenv(
|
||||||
|
"vrobbler_SCRAPER_CONFIG_FILE", "skyscraper.ini"
|
||||||
|
)
|
||||||
|
SCRAPER_SITE = os.getenv("vrobbler_SCRAPER_SITE", "screenscraper")
|
||||||
|
SCRAPER_FRONTEND = os.getenv("vrobbler_FRONTEND", "emulationstation")
|
||||||
|
|
||||||
|
JSON_LOGGING = os.getenv("vrobbler_JSON_LOGGING", False)
|
||||||
|
LOG_TYPE = "json" if JSON_LOGGING else "log"
|
||||||
|
|
||||||
|
default_level = "INFO"
|
||||||
|
if DEBUG:
|
||||||
|
default_level = "DEBUG"
|
||||||
|
|
||||||
|
LOG_LEVEL = os.getenv("vrobbler_LOG_LEVEL", default_level)
|
||||||
|
LOG_FILE_PATH = os.getenv("vrobbler_LOG_FILE_PATH", "/tmp/")
|
||||||
|
|
||||||
|
LOGGING = {
|
||||||
|
"version": 1,
|
||||||
|
"disable_existing_loggers": False,
|
||||||
|
"root": {
|
||||||
|
"handlers": ["console", "file"],
|
||||||
|
"level": LOG_LEVEL,
|
||||||
|
"propagate": True,
|
||||||
|
},
|
||||||
|
"formatters": {
|
||||||
|
"color": {
|
||||||
|
"()": "colorlog.ColoredFormatter",
|
||||||
|
# \r returns caret to line beginning, in tests this eats the silly dot that removes
|
||||||
|
# the beautiful alignment produced below
|
||||||
|
"format": "\r"
|
||||||
|
"{log_color}{levelname:8s}{reset} "
|
||||||
|
"{bold_cyan}{name}{reset}:"
|
||||||
|
"{fg_bold_red}{lineno}{reset} "
|
||||||
|
"{thin_yellow}{funcName} "
|
||||||
|
"{thin_white}{message}"
|
||||||
|
"{reset}",
|
||||||
|
"style": "{",
|
||||||
|
},
|
||||||
|
"log": {"format": "%(asctime)s %(levelname)s %(message)s"},
|
||||||
|
"json": {
|
||||||
|
"()": "pythonjsonlogger.jsonlogger.JsonFormatter",
|
||||||
|
"format": "%(levelname)s %(name) %(funcName) %(lineno) %(asctime)s %(message)s",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"handlers": {
|
||||||
|
"console": {
|
||||||
|
"class": "logging.StreamHandler",
|
||||||
|
"formatter": "color",
|
||||||
|
"level": LOG_LEVEL,
|
||||||
|
},
|
||||||
|
"null": {
|
||||||
|
"class": "logging.NullHandler",
|
||||||
|
"level": LOG_LEVEL,
|
||||||
|
},
|
||||||
|
"file": {
|
||||||
|
"class": "logging.handlers.RotatingFileHandler",
|
||||||
|
"filename": "".join([LOG_FILE_PATH, "vrobbler.log"]),
|
||||||
|
"formatter": LOG_TYPE,
|
||||||
|
"level": LOG_LEVEL,
|
||||||
|
},
|
||||||
|
"requests_file": {
|
||||||
|
"class": "logging.handlers.RotatingFileHandler",
|
||||||
|
"filename": "".join([LOG_FILE_PATH, "vrobbler_requests.log"]),
|
||||||
|
"formatter": LOG_TYPE,
|
||||||
|
"level": LOG_LEVEL,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"loggers": {
|
||||||
|
# Quiet down our console a little
|
||||||
|
"django": {
|
||||||
|
"handlers": ["file"],
|
||||||
|
"propagate": True,
|
||||||
|
},
|
||||||
|
"django.db.backends": {"handlers": ["null"]},
|
||||||
|
"vrobbler": {
|
||||||
|
"handlers": ["console", "file"],
|
||||||
|
"propagate": True,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if DEBUG:
|
||||||
|
# We clear out a db with lots of games all the time in dev
|
||||||
|
DATA_UPLOAD_MAX_NUMBER_FIELDS = 3000
|
||||||
36
vrobbler/urls.py
Normal file
36
vrobbler/urls.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
from django.conf import settings
|
||||||
|
from django.conf.urls.static import static
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.urls import include, path
|
||||||
|
from scrobbles.views import RecentScrobbleList
|
||||||
|
from scrobbles import urls as scrobble_urls
|
||||||
|
|
||||||
|
# from scrobbles.api.views import ScrobbleViewSet
|
||||||
|
# from media_types.api.views import (
|
||||||
|
# ShowViewSet,
|
||||||
|
# MovieViewSet,
|
||||||
|
# )
|
||||||
|
from rest_framework import routers
|
||||||
|
|
||||||
|
# router = routers.DefaultRouter()
|
||||||
|
# router.register(r"scrobbles", ScrobbleViewSet)
|
||||||
|
# router.register(r"shows", ShowViewSet)
|
||||||
|
# router.register(r"movies", MovieViewSet)
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path("admin/", admin.site.urls),
|
||||||
|
# path("api-auth/", include("rest_framework.urls")),
|
||||||
|
# path("api/v1/", include(router.urls)),
|
||||||
|
# path("movies/", include(movies, namespace="movies")),
|
||||||
|
# path("shows/", include(shows, namespace="shows")),
|
||||||
|
path("scrobbles/", include(scrobble_urls, namespace="scrobbles")),
|
||||||
|
path("", RecentScrobbleList.as_view(), name="home"),
|
||||||
|
]
|
||||||
|
|
||||||
|
if settings.DEBUG:
|
||||||
|
urlpatterns += static(
|
||||||
|
settings.MEDIA_URL, document_root=settings.MEDIA_ROOT
|
||||||
|
)
|
||||||
|
urlpatterns += static(
|
||||||
|
settings.STATIC_URL, document_root=settings.STATIC_ROOT
|
||||||
|
)
|
||||||
16
vrobbler/wsgi.py
Normal file
16
vrobbler/wsgi.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
"""
|
||||||
|
WSGI config for vrobbler project.
|
||||||
|
|
||||||
|
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/3.1/howto/deployment/wsgi/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.wsgi import get_wsgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'vrobbler.settings')
|
||||||
|
|
||||||
|
application = get_wsgi_application()
|
||||||
Reference in New Issue
Block a user