Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9088412d1e | |||
| c7339fbe31 | |||
| 4ce3dc03c5 | |||
| 5a4ef678a8 | |||
| 5ca22efeaa | |||
| 912ea8bfac | |||
| b541e1084d | |||
| c9b9da4abc |
35
PROJECT.org
35
PROJECT.org
@ -498,7 +498,33 @@ needed import celery task. This is how the WebDAV celery task currently works.
|
||||
This would also be an opporunity to clean up the code around WebDAV imports
|
||||
and make them more re-usable for other import services.
|
||||
|
||||
** TODO [#B] Add OMDB source as backup when TMDB returns nothing :videos:metadata:imdb:
|
||||
* Version 48.0 [2/2]
|
||||
** DONE [#B] Show team or player images on sport detail and scrobble detail :sports:templates:
|
||||
:PROPERTIES:
|
||||
:ID: 68c17383-ee6e-4b5f-b3f5-1b637a0a3ea8
|
||||
:END:
|
||||
|
||||
*** Description
|
||||
|
||||
On the sport event detail page, we should show the images of the teams or
|
||||
players invovled.
|
||||
|
||||
Also, those images for the sport event should be shown on the scrobble detail
|
||||
page for sport event scrobble details.
|
||||
|
||||
** DONE [#B] Add fix_metadta method to Video instances :videos:metadata:
|
||||
:PROPERTIES:
|
||||
:ID: 9df5404d-1b60-4eee-b7cf-1f7e6dfade65
|
||||
:END:
|
||||
|
||||
*** Description
|
||||
|
||||
Turns out we don't have a fix_metadata method for videos. We should add that using
|
||||
the basic logic from find_or_create on the Video model.
|
||||
|
||||
|
||||
* Version 47.2 [1/1]
|
||||
** DONE [#B] Add OMDB source as backup when TMDB returns nothing :videos:metadata:imdb:
|
||||
:PROPERTIES:
|
||||
:ID: 20195445-7fdd-49be-9767-103b12da0bfb
|
||||
:END:
|
||||
@ -512,6 +538,13 @@ epiodes up in three parts, while they were always broadcast three-in-one, and
|
||||
that's how IMDB lists them. Thus, the IMDB ID means nothing, and the videos end
|
||||
up unenriched.
|
||||
|
||||
|
||||
* Version 47.1 [1/1]
|
||||
** DONE [#A] Untangle the sports migrations errors :sports:bug:migrations:
|
||||
:PROPERTIES:
|
||||
:ID: 4d50ca2e-f45b-dde8-e3c9-cd84f353b349
|
||||
:END:
|
||||
|
||||
* Version 47.0 [1/1]
|
||||
** DONE [#B] Change sports scrobbling a bit :feature:sports:scrobbles:
|
||||
:PROPERTIES:
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "vrobbler"
|
||||
version = "47.0"
|
||||
version = "48.0"
|
||||
description = ""
|
||||
authors = ["Colin Powell <colin@unbl.ink>"]
|
||||
|
||||
|
||||
@ -61,9 +61,7 @@ class BirdSightingLogData(BaseLogData, WithPeopleLogData):
|
||||
@cached_property
|
||||
def bird_list(self) -> str:
|
||||
if self.birds:
|
||||
return ", ".join(
|
||||
[BirdSightingEntry(**b).__str__() for b in self.birds]
|
||||
)
|
||||
return ", ".join([BirdSightingEntry(**b).__str__() for b in self.birds])
|
||||
return ""
|
||||
|
||||
def as_html(self) -> str:
|
||||
@ -80,9 +78,7 @@ class BirdSightingLogData(BaseLogData, WithPeopleLogData):
|
||||
)
|
||||
|
||||
if self.area:
|
||||
html_parts.append(
|
||||
f'<div class="birding-area">Area: {self.area}</div>'
|
||||
)
|
||||
html_parts.append(f'<div class="birding-area">Area: {self.area}</div>')
|
||||
|
||||
if self.party_size:
|
||||
html_parts.append(
|
||||
@ -105,9 +101,7 @@ class BirdSightingLogData(BaseLogData, WithPeopleLogData):
|
||||
)
|
||||
|
||||
if self.guide:
|
||||
html_parts.append(
|
||||
f'<div class="birding-guide">Guide: {self.guide}</div>'
|
||||
)
|
||||
html_parts.append(f'<div class="birding-guide">Guide: {self.guide}</div>')
|
||||
|
||||
if self.duration_minutes:
|
||||
html_parts.append(
|
||||
@ -183,9 +177,7 @@ class Bird(TimeStampedModel):
|
||||
|
||||
class BirdingLocation(ScrobblableMixin):
|
||||
description = models.TextField(**BNULL)
|
||||
geo_location = models.ForeignKey(
|
||||
GeoLocation, **BNULL, on_delete=models.DO_NOTHING
|
||||
)
|
||||
geo_location = models.ForeignKey(GeoLocation, **BNULL, on_delete=models.DO_NOTHING)
|
||||
ebird_hotspot_id = models.CharField(max_length=255, **BNULL)
|
||||
|
||||
def get_absolute_url(self):
|
||||
@ -193,7 +185,7 @@ class BirdingLocation(ScrobblableMixin):
|
||||
|
||||
@property
|
||||
def subtitle(self):
|
||||
return ""
|
||||
return self.geo_location
|
||||
|
||||
@property
|
||||
def strings(self) -> ScrobblableConstants:
|
||||
@ -269,9 +261,7 @@ class BirdingCSVImport(TimeStampedModel):
|
||||
self.save(update_fields=["process_log", "process_count"])
|
||||
return
|
||||
for count, scrobble in enumerate(scrobbles):
|
||||
scrobble_str = (
|
||||
f"{scrobble.id}\t{scrobble.timestamp}\t{scrobble.media_obj}"
|
||||
)
|
||||
scrobble_str = f"{scrobble.id}\t{scrobble.timestamp}\t{scrobble.media_obj}"
|
||||
log_line = f"{scrobble_str}"
|
||||
if count > 0:
|
||||
log_line = "\n" + log_line
|
||||
|
||||
@ -266,8 +266,9 @@ class BoardGame(ScrobblableMixin):
|
||||
"self", **BNULL, on_delete=models.DO_NOTHING
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
@property
|
||||
def subtitle(self) -> str:
|
||||
return self.publisher
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("boardgames:boardgame_detail", kwargs={"slug": self.uuid})
|
||||
|
||||
@ -173,11 +173,7 @@ class Book(LongPlayScrobblableMixin):
|
||||
genre = TaggableManager(through=ObjectWithGenres, blank=True, verbose_name="Genre")
|
||||
|
||||
def __str__(self) -> str:
|
||||
if self.issue_number and "Issue" not in str(self.title):
|
||||
return f"{self.title} - Issue {self.issue_number}"
|
||||
if self.volume_number and "Volume" not in str(self.title):
|
||||
return f"{self.title} - Volume {self.volume_number}"
|
||||
return f"{self.title}"
|
||||
return f"{self.title} - {self.subtitle}"
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self.pages:
|
||||
@ -188,7 +184,12 @@ class Book(LongPlayScrobblableMixin):
|
||||
|
||||
@property
|
||||
def subtitle(self):
|
||||
return f" by {self.author}"
|
||||
subtitle = self.author
|
||||
if self.issue_number and "Issue" not in str(self.title):
|
||||
subtitle += " - Issue {self.issue_number}"
|
||||
if self.volume_number and "Volume" not in str(self.title):
|
||||
subtitle += " - Volume {self.volume_number}"
|
||||
return subtitle
|
||||
|
||||
@property
|
||||
def strings(self) -> ScrobblableConstants:
|
||||
|
||||
@ -20,6 +20,7 @@ class ScrobbleInline(admin.TabularInline):
|
||||
extra = 0
|
||||
raw_id_fields = (
|
||||
"video",
|
||||
"channel",
|
||||
"podcast_episode",
|
||||
"track",
|
||||
"video_game",
|
||||
@ -30,6 +31,7 @@ class ScrobbleInline(admin.TabularInline):
|
||||
"board_game",
|
||||
"geo_location",
|
||||
"task",
|
||||
"puzzle",
|
||||
"mood",
|
||||
"brick_set",
|
||||
"trail",
|
||||
@ -59,47 +61,38 @@ class ImportBaseAdmin(admin.ModelAdmin):
|
||||
|
||||
|
||||
@admin.register(AudioScrobblerTSVImport)
|
||||
class AudioScrobblerTSVImportAdmin(ImportBaseAdmin):
|
||||
...
|
||||
class AudioScrobblerTSVImportAdmin(ImportBaseAdmin): ...
|
||||
|
||||
|
||||
@admin.register(LastFmImport)
|
||||
class LastFmImportAdmin(ImportBaseAdmin):
|
||||
...
|
||||
class LastFmImportAdmin(ImportBaseAdmin): ...
|
||||
|
||||
|
||||
@admin.register(KoReaderImport)
|
||||
class KoReaderImportAdmin(ImportBaseAdmin):
|
||||
...
|
||||
class KoReaderImportAdmin(ImportBaseAdmin): ...
|
||||
|
||||
|
||||
@admin.register(RetroarchImport)
|
||||
class RetroarchImportAdmin(ImportBaseAdmin):
|
||||
...
|
||||
class RetroarchImportAdmin(ImportBaseAdmin): ...
|
||||
|
||||
|
||||
class RetroarchImportAdmin(ImportBaseAdmin):
|
||||
...
|
||||
class RetroarchImportAdmin(ImportBaseAdmin): ...
|
||||
|
||||
|
||||
@admin.register(BGStatsImport)
|
||||
class BGStatsImportAdmin(ImportBaseAdmin):
|
||||
...
|
||||
class BGStatsImportAdmin(ImportBaseAdmin): ...
|
||||
|
||||
|
||||
@admin.register(EBirdCSVImport)
|
||||
class EBirdCSVImportAdmin(ImportBaseAdmin):
|
||||
...
|
||||
class EBirdCSVImportAdmin(ImportBaseAdmin): ...
|
||||
|
||||
|
||||
@admin.register(ScaleCSVImport)
|
||||
class ScaleCSVImportAdmin(ImportBaseAdmin):
|
||||
...
|
||||
class ScaleCSVImportAdmin(ImportBaseAdmin): ...
|
||||
|
||||
|
||||
@admin.register(TrailGPXImport)
|
||||
class TrailGPXImportAdmin(ImportBaseAdmin):
|
||||
...
|
||||
class TrailGPXImportAdmin(ImportBaseAdmin): ...
|
||||
|
||||
|
||||
@admin.register(Genre)
|
||||
|
||||
@ -65,6 +65,9 @@ class ScrobblableMixin(TimeStampedModel):
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.title} - {self.subtitle}"
|
||||
|
||||
@property
|
||||
def run_time_seconds(self) -> int:
|
||||
run_time = 900
|
||||
|
||||
@ -184,7 +184,7 @@ class ScrobbleableDetailView(ChartContextMixin, DetailView):
|
||||
def get_context_data(self, **kwargs):
|
||||
context_data = super().get_context_data(**kwargs)
|
||||
scrobbles = []
|
||||
if not self.request.user.is_anonymous:
|
||||
if not self.request.user.is_anonymous and hasattr(self.object, "scrobble_set"):
|
||||
scrobbles = self.object.scrobble_set.filter(
|
||||
user=self.request.user
|
||||
).order_by("-timestamp")
|
||||
@ -439,8 +439,7 @@ class ScrobbleListView(LoginRequiredMixin, ListView):
|
||||
full_qs = getattr(self, "_full_queryset", None)
|
||||
if full_qs is not None and getattr(self, "tag_list", []):
|
||||
total = (
|
||||
full_qs.aggregate(total=Sum("playback_position_seconds"))["total"]
|
||||
or 0
|
||||
full_qs.aggregate(total=Sum("playback_position_seconds"))["total"] or 0
|
||||
)
|
||||
ctx["total_time_seconds"] = total
|
||||
return ctx
|
||||
@ -1245,8 +1244,7 @@ class ScrobbleDetailView(DetailView):
|
||||
track=media_obj, user=self.object.user
|
||||
).order_by("-timestamp")[:20]
|
||||
context["has_mopidy_uri"] = any(
|
||||
(s.log or {}).get("raw_data", {}).get("mopidy_uri")
|
||||
for s in scrobbles
|
||||
(s.log or {}).get("raw_data", {}).get("mopidy_uri") for s in scrobbles
|
||||
)
|
||||
else:
|
||||
context["has_mopidy_uri"] = False
|
||||
|
||||
@ -108,20 +108,4 @@ class Migration(migrations.Migration):
|
||||
migrations.RunPython(
|
||||
migrate_sport_event_data, reverse_code=migrations.RunPython.noop
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="sportevent",
|
||||
name="home_team",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="sportevent",
|
||||
name="away_team",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="sportevent",
|
||||
name="player_one",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="sportevent",
|
||||
name="player_two",
|
||||
),
|
||||
]
|
||||
|
||||
@ -0,0 +1,27 @@
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("sports", "0021_team_logo"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name="sportevent",
|
||||
name="home_team",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="sportevent",
|
||||
name="away_team",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="sportevent",
|
||||
name="player_one",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="sportevent",
|
||||
name="player_two",
|
||||
),
|
||||
]
|
||||
@ -123,17 +123,17 @@ class SportEvent(ScrobblableMixin):
|
||||
super(SportEvent, self).save(*args, **kwargs)
|
||||
|
||||
def __str__(self) -> str:
|
||||
league = self.league
|
||||
if self.league and self.league.abbreviation_str:
|
||||
league = self.league.abbreviation_str
|
||||
return f"{self.title} - {league}"
|
||||
return f"{self.title} - {self.subtitle}"
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("sports:event_detail", kwargs={"slug": self.uuid})
|
||||
|
||||
@property
|
||||
def subtitle(self) -> str:
|
||||
return self.comp_str
|
||||
league = self.league
|
||||
if self.league and self.league.abbreviation_str:
|
||||
league = self.league.abbreviation_str
|
||||
return f"{league} {self.get_event_type_display()}"
|
||||
|
||||
@property
|
||||
def comp_str(self) -> str:
|
||||
|
||||
@ -163,12 +163,9 @@ class VideoGame(LongPlayScrobblableMixin):
|
||||
platforms = models.ManyToManyField(VideoGamePlatform)
|
||||
retroarch_name = models.CharField(max_length=255, **BNULL)
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
@property
|
||||
def subtitle(self):
|
||||
return f" On {self.platforms.first()}"
|
||||
return f"{self.platforms.first()}"
|
||||
|
||||
@property
|
||||
def strings(self) -> ScrobblableConstants:
|
||||
|
||||
@ -71,6 +71,7 @@ class VideoMetadata:
|
||||
twitch_id: Optional[str] = "",
|
||||
base_run_time_seconds: int = 900,
|
||||
):
|
||||
self.title = ""
|
||||
self.imdb_id = imdb_id
|
||||
self.youtube_id = youtube_id
|
||||
self.twitch_id = twitch_id
|
||||
|
||||
@ -20,6 +20,7 @@ from scrobbles.mixins import (
|
||||
)
|
||||
from taggit.managers import TaggableManager
|
||||
from videos.metadata import VideoMetadata
|
||||
from videos.sources.omdb import lookup_video_from_omdb
|
||||
from videos.sources.tmdb import lookup_video_from_tmdb
|
||||
from videos.sources.youtube import lookup_video_from_youtube
|
||||
|
||||
@ -224,6 +225,8 @@ class Series(TimeStampedModel):
|
||||
|
||||
def is_episode_playing(self, user_id: int) -> bool:
|
||||
last_scrobble = self.scrobbles_for_user(user_id, include_playing=True).first()
|
||||
if not last_scrobble:
|
||||
return False
|
||||
return not last_scrobble.played_to_completion
|
||||
|
||||
def fix_metadata(self, force_update=False):
|
||||
@ -255,9 +258,20 @@ class Series(TimeStampedModel):
|
||||
logger.info("Series not created and overwrite=False, returning")
|
||||
return series
|
||||
|
||||
vdict, _, cover, genres = lookup_video_from_tmdb(
|
||||
imdb_id
|
||||
).as_dict_with_cover_and_genres()
|
||||
try:
|
||||
metadata = lookup_video_from_tmdb(imdb_id)
|
||||
except Exception as e:
|
||||
logger.warning(f"TMDB lookup failed for series {imdb_id}: {e}")
|
||||
metadata = None
|
||||
|
||||
if not metadata or not metadata.title:
|
||||
metadata = lookup_video_from_omdb(imdb_id)
|
||||
|
||||
if not metadata or not metadata.title:
|
||||
logger.warning(f"No metadata found for series {imdb_id} from TMDB or OMDB")
|
||||
return series
|
||||
|
||||
vdict, _, cover, genres = metadata.as_dict_with_cover_and_genres()
|
||||
vdict.pop("video_type")
|
||||
|
||||
vdict["name"] = vdict.pop("title")
|
||||
@ -406,6 +420,84 @@ class Video(ScrobblableMixin):
|
||||
fname = f"{self.title}_{self.uuid}.jpg"
|
||||
self.cover_image.save(fname, ContentFile(r.content), save=True)
|
||||
|
||||
def fix_metadata(self, force_update: bool = False) -> None:
|
||||
if self.video_type == self.VideoType.YOUTUBE and self.youtube_id:
|
||||
vdict, _, cover, genres = lookup_video_from_youtube(
|
||||
self.youtube_id
|
||||
).as_dict_with_cover_and_genres()
|
||||
|
||||
for k, v in vdict.items():
|
||||
setattr(self, k, v)
|
||||
self.save()
|
||||
|
||||
if cover:
|
||||
self.save_image_from_url(cover)
|
||||
if genres:
|
||||
self.genre.add(*genres)
|
||||
return
|
||||
|
||||
if self.video_type == self.VideoType.TWITCH and self.twitch_id:
|
||||
from videos.sources.twitch import lookup_video_from_twitch
|
||||
|
||||
metadata = lookup_video_from_twitch(self.twitch_id)
|
||||
self.title = metadata.title or f"Twitch VOD {self.twitch_id}"
|
||||
self.overview = metadata.overview
|
||||
self.base_run_time_seconds = metadata.base_run_time_seconds
|
||||
if metadata.upload_date:
|
||||
self.upload_date = metadata.upload_date
|
||||
if metadata.year:
|
||||
self.year = metadata.year
|
||||
self.video_type = Video.VideoType.TWITCH
|
||||
|
||||
if metadata.channel_id:
|
||||
from videos.models import Channel
|
||||
|
||||
self.channel = Channel.objects.filter(
|
||||
id=metadata.channel_id
|
||||
).first()
|
||||
|
||||
self.save()
|
||||
|
||||
if metadata.cover_url:
|
||||
self.save_image_from_url(metadata.cover_url)
|
||||
return
|
||||
|
||||
if self.imdb_id:
|
||||
try:
|
||||
metadata = lookup_video_from_tmdb(self.imdb_id)
|
||||
except Exception as e:
|
||||
logger.warning(f"TMDB lookup failed for {self.imdb_id}: {e}")
|
||||
metadata = None
|
||||
|
||||
if not metadata or not metadata.title:
|
||||
metadata = lookup_video_from_omdb(self.imdb_id)
|
||||
|
||||
if not metadata or not metadata.title:
|
||||
logger.warning(f"No metadata found for {self} from TMDB or OMDB")
|
||||
return
|
||||
|
||||
vdict, series_id, cover, genres = (
|
||||
metadata.as_dict_with_cover_and_genres()
|
||||
)
|
||||
|
||||
for k, v in vdict.items():
|
||||
setattr(self, k, v)
|
||||
|
||||
if series_id:
|
||||
self.tv_series = Series.find_or_create(imdb_id=series_id)
|
||||
|
||||
self.save()
|
||||
|
||||
if cover:
|
||||
self.save_image_from_url(cover)
|
||||
if genres:
|
||||
self.genre.add(*genres)
|
||||
return
|
||||
|
||||
logger.warning(
|
||||
f"No metadata source available for {self} (type={self.video_type})"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_from_youtube_id(cls, youtube_id: str, overwrite: bool = False) -> "Video":
|
||||
video, created = cls.objects.get_or_create(youtube_id=youtube_id)
|
||||
@ -432,9 +524,20 @@ class Video(ScrobblableMixin):
|
||||
if not created and not overwrite:
|
||||
return video
|
||||
|
||||
vdict, series_id, cover, genres = lookup_video_from_tmdb(
|
||||
imdb_id
|
||||
).as_dict_with_cover_and_genres()
|
||||
try:
|
||||
metadata = lookup_video_from_tmdb(imdb_id)
|
||||
except Exception as e:
|
||||
logger.warning(f"TMDB lookup failed for {imdb_id}: {e}")
|
||||
metadata = None
|
||||
|
||||
if not metadata or not metadata.title:
|
||||
metadata = lookup_video_from_omdb(imdb_id)
|
||||
|
||||
if not metadata or not metadata.title:
|
||||
logger.warning(f"No metadata found for {imdb_id} from TMDB or OMDB")
|
||||
return video
|
||||
|
||||
vdict, series_id, cover, genres = metadata.as_dict_with_cover_and_genres()
|
||||
|
||||
if created or overwrite:
|
||||
for k, v in vdict.items():
|
||||
|
||||
83
vrobbler/apps/videos/sources/omdb.py
Normal file
83
vrobbler/apps/videos/sources/omdb.py
Normal file
@ -0,0 +1,83 @@
|
||||
import logging
|
||||
import re
|
||||
from typing import Optional
|
||||
|
||||
import requests
|
||||
from django.conf import settings
|
||||
from videos.metadata import VideoMetadata, VideoType
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
OMDB_API_KEY = getattr(settings, "OMDB_API_KEY", "")
|
||||
|
||||
OMDB_URL = "https://www.omdbapi.com/"
|
||||
RUNTIME_RE = re.compile(r"(\d+)\s*min")
|
||||
|
||||
|
||||
def lookup_video_from_omdb(imdb_id: str) -> Optional[VideoMetadata]:
|
||||
if not imdb_id.startswith("tt"):
|
||||
imdb_id = f"tt{imdb_id}"
|
||||
|
||||
if not OMDB_API_KEY:
|
||||
logger.warning("No OMDB API key configured")
|
||||
return None
|
||||
|
||||
params = {"apikey": OMDB_API_KEY, "i": imdb_id, "plot": "full"}
|
||||
|
||||
try:
|
||||
response = requests.get(OMDB_URL, params=params)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(f"OMDB API error for {imdb_id}: {e}")
|
||||
return None
|
||||
|
||||
if data.get("Response") == "False":
|
||||
logger.info(f"OMDB no result for {imdb_id}: {data.get('Error')}")
|
||||
return None
|
||||
|
||||
metadata = VideoMetadata(imdb_id=imdb_id)
|
||||
metadata.title = data.get("Title")
|
||||
metadata.plot = data.get("Plot")
|
||||
metadata.overview = data.get("Plot")
|
||||
|
||||
raw_year = data.get("Year")
|
||||
if raw_year and raw_year.isdigit():
|
||||
metadata.year = int(raw_year)
|
||||
|
||||
raw_rating = data.get("imdbRating")
|
||||
if raw_rating and raw_rating != "N/A":
|
||||
metadata.imdb_rating = raw_rating
|
||||
|
||||
raw_cover = data.get("Poster")
|
||||
if raw_cover and raw_cover != "N/A":
|
||||
metadata.cover_url = raw_cover
|
||||
|
||||
raw_runtime = data.get("Runtime")
|
||||
if raw_runtime:
|
||||
match = RUNTIME_RE.match(raw_runtime)
|
||||
if match:
|
||||
metadata.base_run_time_seconds = int(match.group(1)) * 60
|
||||
|
||||
media_type = data.get("Type")
|
||||
if media_type == "movie":
|
||||
metadata.video_type = VideoType.MOVIE.value
|
||||
elif media_type in ("series", "episode"):
|
||||
metadata.video_type = VideoType.TV_EPISODE.value
|
||||
|
||||
if media_type == "episode":
|
||||
raw_season = data.get("Season")
|
||||
if raw_season and raw_season != "N/A":
|
||||
metadata.season_number = int(raw_season)
|
||||
raw_episode = data.get("Episode")
|
||||
if raw_episode and raw_episode != "N/A":
|
||||
metadata.episode_number = int(raw_episode)
|
||||
series_imdb_id = data.get("seriesID")
|
||||
if series_imdb_id and series_imdb_id != "N/A":
|
||||
metadata.tv_series_imdb_id = series_imdb_id
|
||||
|
||||
raw_genres = data.get("Genre")
|
||||
if raw_genres:
|
||||
metadata.genres = [g.strip() for g in raw_genres.split(",") if g.strip()]
|
||||
|
||||
return metadata
|
||||
@ -3,14 +3,11 @@ import logging
|
||||
import pendulum
|
||||
from django.conf import settings
|
||||
from themoviedb import TMDb
|
||||
from tmdbv3api import TV, Movie, TMDb as TMDb_direct
|
||||
from tmdbv3api import TV, Movie
|
||||
from videos.metadata import VideoMetadata, VideoType
|
||||
|
||||
TMDB_KEY = getattr(settings, "TMDB_API_KEY", "")
|
||||
|
||||
tmdb_direct = TMDb_direct()
|
||||
tmdb_direct.api_key = TMDB_KEY
|
||||
|
||||
tmdb = TMDb(key=TMDB_KEY, language="en-US", region="US")
|
||||
|
||||
TMDB_IMAGE_URL = "https://image.tmdb.org/t/p/original"
|
||||
@ -36,7 +33,7 @@ def lookup_video_from_tmdb(name_or_id: str, kind: str = "movie") -> VideoMetadat
|
||||
video_metadata = VideoMetadata(imdb_id=imdb_id)
|
||||
|
||||
media = None
|
||||
show = None
|
||||
show_data = None
|
||||
if len(tmdb_result.movie_results) > 0:
|
||||
media = Movie().details(tmdb_result.movie_results[0].id)
|
||||
video_metadata.video_type = VideoType.MOVIE.value
|
||||
|
||||
@ -32,7 +32,11 @@ class SeriesDetailView(LoginRequiredMixin, ChartContextMixin, generic.DetailView
|
||||
context_data = super().get_context_data(**kwargs)
|
||||
|
||||
context_data["scrobbles"] = self.object.scrobbles_for_user(user_id)
|
||||
next_episode_id = self.object.last_scrobbled_episode(user_id).next_imdb_id or ""
|
||||
next_episode_id = ""
|
||||
if self.object.last_scrobbled_episode(user_id):
|
||||
next_episode_id = (
|
||||
self.object.last_scrobbled_episode(user_id).next_imdb_id or ""
|
||||
)
|
||||
if self.object.is_episode_playing(user_id):
|
||||
next_episode_id = ""
|
||||
if next_episode_id:
|
||||
|
||||
@ -60,6 +60,7 @@ THEAUDIODB_API_KEY = os.getenv("VROBBLER_THEAUDIODB_API_KEY", "2")
|
||||
PODCASTINDEX_API_KEY = os.getenv("VROBBLER_PODCASTINDEX_API_KEY", "")
|
||||
PODCASTINDEX_API_SECRET = os.getenv("VROBBLER_PODCASTINDEX_API_SECRET", "")
|
||||
TMDB_API_KEY = os.getenv("VROBBLER_TMDB_API_KEY", "")
|
||||
OMDB_API_KEY = os.getenv("VROBBLER_OMDB_API_KEY", "")
|
||||
LASTFM_API_KEY = os.getenv("VROBBLER_LASTFM_API_KEY")
|
||||
LASTFM_SECRET_KEY = os.getenv("VROBBLER_LASTFM_SECRET_KEY")
|
||||
IGDB_CLIENT_ID = os.getenv("VROBBLER_IGDB_CLIENT_ID")
|
||||
|
||||
@ -50,7 +50,11 @@
|
||||
|
||||
<h1 class="d-flex align-items-center gap-2">
|
||||
{% if object.media_type == "Video" %}🎬{% elif object.media_type == "Track" %}🎵{% elif object.media_type == "PodcastEpisode" %}🎙️{% elif object.media_type == "SportEvent" %}⚽{% elif object.media_type == "Book" %}📚{% elif object.media_type == "Paper" %}📄{% elif object.media_type == "VideoGame" %}🎮{% elif object.media_type == "BoardGame" %}🎲{% elif object.media_type == "GeoLocation" %}📍{% elif object.media_type == "Trail" %}🥾{% elif object.media_type == "Beer" %}🍺{% elif object.media_type == "Puzzle" %}🧩{% elif object.media_type == "Food" %}🍔{% elif object.media_type == "Task" %}✅{% elif object.media_type == "WebPage" %}🌐{% elif object.media_type == "LifeEvent" %}🎉{% elif object.media_type == "Mood" %}😊{% elif object.media_type == "BrickSet" %}🧱{% elif object.media_type == "Channel" %}📺{% endif %}
|
||||
{% if object.media_obj.get_absolute_url %}<a href="{{ object.media_obj.get_absolute_url }}">{% endif %}{{ object.media_obj }}{% if object.media_obj.get_absolute_url %}</a>{% endif %}
|
||||
{% if object.media_obj.get_absolute_url %}
|
||||
<a href="{{ object.media_obj.get_absolute_url }}">{% endif %}
|
||||
{{ object.media_obj.title }}
|
||||
{% if object.media_obj.get_absolute_url %}</a>
|
||||
{% endif %}
|
||||
{% if user.is_authenticated and object.media_obj %}
|
||||
<button id="favorite-btn"
|
||||
data-url="{% url 'scrobbles:toggle-favorite' object.media_type object.media_obj.id %}"
|
||||
@ -62,7 +66,15 @@
|
||||
</button>
|
||||
{% endif %}
|
||||
</h1>
|
||||
<h2>{{ object.media_obj.subtitle }}</h2>
|
||||
<p>
|
||||
{% if object.media_type == "SportEvent" %}
|
||||
{% for team in object.media_obj.teams.all %}
|
||||
<img src="{{team.logo.url}}" width=150 />
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% if object.media_type == "Task" and object.logdata.title %}
|
||||
</p>
|
||||
<h2>{{ object.logdata.title }}</h2>
|
||||
{% endif %}
|
||||
<h3 class="text-muted">{{ object.local_timestamp }}</h3>
|
||||
|
||||
@ -1,13 +1,16 @@
|
||||
{% extends "base_detail.html" %}
|
||||
|
||||
{% block title %}{{object}}{% endblock %}
|
||||
{% block title %}{{object.title}}{% endblock %}
|
||||
|
||||
{% block details %}
|
||||
<h2>{{object.subtitle}}</h2>
|
||||
<h2>{{object.league}} {{object.get_event_type_display}}</h2>
|
||||
|
||||
<div class="row">
|
||||
<h2>{{object.tv_series}}</h2>
|
||||
<div class="col-md">
|
||||
{% for team in object.teams.all %}
|
||||
<img src="{{team.logo.url}}" width=150 />
|
||||
{% endfor %}
|
||||
<hr />
|
||||
<h3>Last scrobbles</h3>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-sm">
|
||||
|
||||
Reference in New Issue
Block a user