Compare commits

...

7 Commits
54.2 ... 54.5

Author SHA1 Message Date
0f1882b21f [release] Bump to version 54.5
All checks were successful
build / test (push) Successful in 2m7s
deploy / build-and-deploy (push) Has been skipped
deploy / test (push) Successful in 1m58s
- Fix bug in generating mood trends
2026-06-17 21:46:21 -04:00
e819a2db0d [trends] Fix bug in mood trend generation 2026-06-17 21:45:45 -04:00
e03cf6c9b1 [release] Bump to version 54.4
All checks were successful
build / test (push) Successful in 2m0s
deploy / test (push) Successful in 1m58s
deploy / build-and-deploy (push) Successful in 29s
- Remove all-time trends
- Add a trend around moods
2026-06-17 17:09:30 -04:00
471e70ff7f [trends] Remove all time trends
Some checks failed
build / test (push) Has been cancelled
2026-06-17 17:09:11 -04:00
255e335d7a [trends] Add some mood related trends 2026-06-17 17:07:16 -04:00
c8cf80b513 [release] Bump to version 54.3
All checks were successful
deploy / test (push) Successful in 2m1s
build / test (push) Successful in 1m52s
deploy / build-and-deploy (push) Successful in 29s
- Fix bug in series metadata cleanup script
2026-06-17 12:05:50 -04:00
b4180afbed [videos] Fix metadata for series script
Some checks failed
build / test (push) Has been cancelled
2026-06-17 12:05:22 -04:00
16 changed files with 642 additions and 26 deletions

View File

@ -590,6 +590,33 @@ We should rename `email_scrobble_board_game` to reflect the fact that it's just
a helper method to create board game scrobbles given a json blob. It's
independent of the email flow it was originally creatdd for
* Version 54.5 [1/1]
** DONE Fix bug in generating mood trends :trends:
:PROPERTIES:
:ID: 8e75abfa-8e70-d85b-00a4-a4813bbce879
:END:
* Version 54.4 [2/2]
** DONE [#A] Remove all-time trends :trends:
:PROPERTIES:
:ID: 53b231d1-7677-8cd3-1d88-dae110aba1e6
:END:
*** Description
All time trends take forever to calculate and don't provide too much data
** DONE [#B] Add a trend around moods :moods:trends:
:PROPERTIES:
:ID: fba3f4ae-8f97-ee0b-e762-31630884518a
:END:
* Version 54.3 [1/1]
** DONE [#B] Fix bug in series metadata cleanup script :videos:metadta:
:PROPERTIES:
:ID: 85448702-907c-5d63-f5af-7795661d7c46
:END:
* Version 54.2 [4/4]
** DONE [#B] Add script to clean up TV series metadata :videos:metadata:
:PROPERTIES:

View File

@ -1,6 +1,6 @@
[tool.poetry]
name = "vrobbler"
version = "54.2"
version = "54.5"
description = ""
authors = ["Colin Powell <colin@unbl.ink>"]

View File

@ -8,7 +8,6 @@ PERIOD_CHOICES = [
("last_30", "Last 30 days"),
("last_90", "Last 90 days"),
("last_year", "Last year"),
("all_time", "All time"),
]
@ -18,7 +17,7 @@ class TrendResult(TimeStampedModel):
period = models.CharField(
max_length=20,
choices=PERIOD_CHOICES,
default="all_time",
default="last_30",
)
computed_at = models.DateTimeField(auto_now_add=True)
data = models.JSONField(default=dict)

View File

@ -0,0 +1,78 @@
<div class="row">
<div class="col-md-6">
<h5 class="mb-3">By Hour of Day</h5>
{% if data.hours %}
<div class="table-responsive">
<table class="table table-striped table-sm">
<thead>
<tr>
<th>Hour</th>
<th class="text-end">Avg Quality</th>
<th class="text-end">Check-ins</th>
</tr>
</thead>
<tbody>
{% for entry in data.hours %}
{% if entry.count > 0 %}
<tr>
<td>
{% if entry.hour == 0 %}
12 AM
{% elif entry.hour < 12 %}
{{ entry.hour }} AM
{% elif entry.hour == 12 %}
12 PM
{% else %}
{{ entry.hour|add:"-12" }} PM
{% endif %}
</td>
<td class="text-end">
<span class="{% if entry.avg_quality >= 5 %}text-success{% elif entry.avg_quality >= 4 %}text-info{% elif entry.avg_quality >= 3 %}text-warning{% else %}text-danger{% endif %}">
{{ entry.avg_quality }}
</span>
</td>
<td class="text-end">{{ entry.count }}</td>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p class="text-muted">No hourly data.</p>
{% endif %}
</div>
<div class="col-md-6">
<h5 class="mb-3">By Day of Week</h5>
{% if data.days %}
<div class="table-responsive">
<table class="table table-striped table-sm">
<thead>
<tr>
<th>Day</th>
<th class="text-end">Avg Quality</th>
<th class="text-end">Check-ins</th>
</tr>
</thead>
<tbody>
{% for entry in data.days %}
{% if entry.count > 0 %}
<tr>
<td>{{ entry.day_name }}</td>
<td class="text-end">
<span class="{% if entry.avg_quality >= 5 %}text-success{% elif entry.avg_quality >= 4 %}text-info{% elif entry.avg_quality >= 3 %}text-warning{% else %}text-danger{% endif %}">
{{ entry.avg_quality }}
</span>
</td>
<td class="text-end">{{ entry.count }}</td>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p class="text-muted">No daily data.</p>
{% endif %}
</div>
</div>

View File

@ -0,0 +1,45 @@
<div class="row">
<div class="col-12">
{% if data.moods %}
<p class="text-muted mb-3">
Total mood check-ins{% if current_period_label %} ({{ current_period_label }}){% endif %}: <strong>{{ data.total }}</strong>
&middot; Positive: <strong>{{ data.positive_count }}</strong>
&middot; Negative: <strong>{{ data.negative_count }}</strong>
</p>
<div class="table-responsive">
<table class="table table-striped table-sm">
<thead>
<tr>
<th>Mood</th>
<th class="text-end">Count</th>
<th>Distribution</th>
</tr>
</thead>
<tbody>
{% with max=data.moods.0.count %}
{% for entry in data.moods %}
<tr>
<td>{{ entry.mood }}</td>
<td class="text-end">{{ entry.count }}</td>
<td style="width: 40%;">
{% if max > 0 %}
<div class="progress" style="height: 12px;">
<div class="progress-bar" role="progressbar"
style="width: {% widthratio entry.count max 100 %}%;"
aria-valuenow="{{ entry.count }}"
aria-valuemin="0" aria-valuemax="{{ max }}">
</div>
</div>
{% endif %}
</td>
</tr>
{% endfor %}
{% endwith %}
</tbody>
</table>
</div>
{% else %}
<p class="text-muted">No mood distribution data found.</p>
{% endif %}
</div>
</div>

View File

@ -0,0 +1,47 @@
<div class="row">
<div class="col-12">
{% if data.current_streak %}
<div class="alert alert-info">
<strong>Current streak:</strong>
{{ data.current_streak.length }} consecutive
<span class="{% if data.current_streak.mood_type == 'positive' %}text-success{% else %}text-danger{% endif %}">
{{ data.current_streak.mood_type }}
</span>
check-ins since {{ data.current_streak.start_date }}.
</div>
{% endif %}
{% if data.streaks %}
<div class="table-responsive">
<table class="table table-striped table-sm">
<thead>
<tr>
<th>#</th>
<th>Mood Type</th>
<th class="text-end">Length</th>
<th>Start</th>
<th>End</th>
</tr>
</thead>
<tbody>
{% for streak in data.streaks %}
<tr>
<td>{{ forloop.counter }}</td>
<td>
<span class="{% if streak.mood_type == 'positive' %}text-success{% elif streak.mood_type == 'negative' %}text-danger{% endif %}">
{{ streak.mood_type|title }}
</span>
</td>
<td class="text-end">{{ streak.length }}</td>
<td>{{ streak.start_date }}</td>
<td>{{ streak.end_date }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p class="text-muted">No streak data found.</p>
{% endif %}
</div>
</div>

View File

@ -0,0 +1,39 @@
<div class="row">
<div class="col-12">
{% if data.trajectory %}
<div class="table-responsive">
<table class="table table-striped table-sm">
<thead>
<tr>
<th>Date</th>
<th class="text-end">Avg Quality</th>
<th class="text-end">Check-ins</th>
<th>Mood Bar</th>
</tr>
</thead>
<tbody>
{% for entry in data.trajectory %}
<tr>
<td>{{ entry.date }}</td>
<td class="text-end">{{ entry.avg_quality }}</td>
<td class="text-end">{{ entry.count }}</td>
<td style="width: 40%;">
<div class="progress" style="height: 16px;">
<div class="progress-bar {% if entry.avg_quality >= 5 %}bg-success{% elif entry.avg_quality >= 4 %}bg-info{% elif entry.avg_quality >= 3 %}bg-warning{% else %}bg-danger{% endif %}"
role="progressbar"
style="width: {% widthratio entry.avg_quality 7 100 %}%;"
aria-valuenow="{{ entry.avg_quality }}"
aria-valuemin="1" aria-valuemax="7">
</div>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p class="text-muted">No mood check-in data found.</p>
{% endif %}
</div>
</div>

View File

@ -0,0 +1,64 @@
<div class="row">
<div class="col-md-6">
<h5 class="mb-3">By Weather Condition</h5>
{% if data.conditions %}
<div class="table-responsive">
<table class="table table-striped table-sm">
<thead>
<tr>
<th>Condition</th>
<th class="text-end">Avg Quality</th>
<th class="text-end">Check-ins</th>
</tr>
</thead>
<tbody>
{% for entry in data.conditions %}
<tr>
<td>{{ entry.condition }}</td>
<td class="text-end">
<span class="{% if entry.avg_quality >= 5 %}text-success{% elif entry.avg_quality >= 4 %}text-info{% elif entry.avg_quality >= 3 %}text-warning{% else %}text-danger{% endif %}">
{{ entry.avg_quality }}
</span>
</td>
<td class="text-end">{{ entry.count }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p class="text-muted">No weather-linked mood data found.</p>
{% endif %}
</div>
<div class="col-md-6">
<h5 class="mb-3">By Temperature Range</h5>
{% if data.temp_ranges %}
<div class="table-responsive">
<table class="table table-striped table-sm">
<thead>
<tr>
<th>Temp Range</th>
<th class="text-end">Avg Quality</th>
<th class="text-end">Check-ins</th>
</tr>
</thead>
<tbody>
{% for entry in data.temp_ranges %}
<tr>
<td>{{ entry.range }}</td>
<td class="text-end">
<span class="{% if entry.avg_quality >= 5 %}text-success{% elif entry.avg_quality >= 4 %}text-info{% elif entry.avg_quality >= 3 %}text-warning{% else %}text-danger{% endif %}">
{{ entry.avg_quality }}
</span>
</td>
<td class="text-end">{{ entry.count }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p class="text-muted">No temperature-linked mood data found.</p>
{% endif %}
</div>
</div>

View File

@ -67,5 +67,20 @@
{% elif trend.slug == "activity-distribution" %}
{% include "trends/_activity_distribution.html" %}
{% elif trend.slug == "mood-trajectory" %}
{% include "trends/_mood_trajectory.html" %}
{% elif trend.slug == "mood-by-time" %}
{% include "trends/_mood_by_time.html" %}
{% elif trend.slug == "mood-distribution" %}
{% include "trends/_mood_distribution.html" %}
{% elif trend.slug == "mood-streaks" %}
{% include "trends/_mood_streaks.html" %}
{% elif trend.slug == "mood-weather" %}
{% include "trends/_mood_weather.html" %}
{% endif %}
{% endblock %}

View File

@ -7,6 +7,13 @@ from trends.trends.concurrent import (
compute_concurrent_listening,
compute_concurrent_reading,
)
from trends.trends.mood import (
compute_mood_by_time,
compute_mood_distribution,
compute_mood_streaks,
compute_mood_trajectory,
compute_mood_weather,
)
from trends.trends.reading import compute_reading_pace_vs_activity
from trends.trends.trending import compute_trending_up
@ -28,6 +35,11 @@ compute_activity_distribution = register("activity-distribution")(
# compute_concurrent_listening
# )
compute_concurrent_reading = register("concurrent-reading")(compute_concurrent_reading)
compute_mood_by_time = register("mood-by-time")(compute_mood_by_time)
compute_mood_distribution = register("mood-distribution")(compute_mood_distribution)
compute_mood_streaks = register("mood-streaks")(compute_mood_streaks)
compute_mood_trajectory = register("mood-trajectory")(compute_mood_trajectory)
compute_mood_weather = register("mood-weather")(compute_mood_weather)
compute_peak_hours = register("peak-hours")(compute_peak_hours)
compute_reading_pace_vs_activity = register("reading-pace-vs-activity")(
compute_reading_pace_vs_activity

View File

@ -0,0 +1,208 @@
from collections import Counter, defaultdict
from django.db.models import Q
from scrobbles.models import Scrobble
def _mood_scrobbles(user, period="last_30"):
from trends.utils import get_date_range
start, end = get_date_range(period)
filters = Q(user=user, media_type=Scrobble.MediaType.MOOD)
if start:
filters &= Q(timestamp__gte=start)
if end:
filters &= Q(timestamp__lte=end)
return Scrobble.objects.filter(filters).select_related("mood")
def _parse_quality(raw):
try:
return int(raw)
except (TypeError, ValueError):
return None
def _avg_quality(values):
nums = [v for v in values if v is not None]
if not nums:
return 0.0
return round(sum(nums) / len(nums), 2)
def compute_mood_trajectory(user, period="last_30"):
scrobbles = _mood_scrobbles(user, period).order_by("timestamp")
by_date = defaultdict(list)
for s in scrobbles:
quality = _parse_quality(s.log.get("mood_quality"))
if quality is not None:
day_key = s.timestamp.strftime("%Y-%m-%d")
by_date[day_key].append(quality)
trajectory = []
for date_key in sorted(by_date):
values = by_date[date_key]
trajectory.append(
{
"date": date_key,
"avg_quality": _avg_quality(values),
"count": len(values),
}
)
return {"trajectory": trajectory}
def compute_mood_by_time(user, period="last_30"):
scrobbles = _mood_scrobbles(user, period)
by_hour = defaultdict(list)
by_day = defaultdict(list)
for s in scrobbles:
quality = _parse_quality(s.log.get("mood_quality"))
if quality is not None and s.timestamp:
by_hour[s.timestamp.hour].append(quality)
by_day[s.timestamp.isoweekday()].append(quality)
hours = []
for h in range(24):
vals = by_hour.get(h, [])
hours.append(
{
"hour": h,
"avg_quality": _avg_quality(vals),
"count": len(vals),
}
)
DAY_NAMES = {
1: "Monday",
2: "Tuesday",
3: "Wednesday",
4: "Thursday",
5: "Friday",
6: "Saturday",
7: "Sunday",
}
days = []
for d in range(1, 8):
vals = by_day.get(d, [])
days.append(
{
"day_index": d,
"day_name": DAY_NAMES[d],
"avg_quality": _avg_quality(vals),
"count": len(vals),
}
)
return {"hours": hours, "days": days}
def compute_mood_distribution(user, period="last_30"):
scrobbles = _mood_scrobbles(user, period)
mood_counts = Counter()
type_counts = Counter()
for s in scrobbles:
if s.mood and s.mood.title:
mood_counts[s.mood.title] += 1
mood_type = s.log.get("mood_type")
if mood_type:
type_counts[mood_type] += 1
moods = [
{"mood": mood, "count": count}
for mood, count in mood_counts.most_common()
]
total = sum(mood_counts.values())
return {
"moods": moods,
"total": total,
"positive_count": type_counts.get("positive", 0),
"negative_count": type_counts.get("negative", 0),
}
def compute_mood_streaks(user, period="last_30"):
scrobbles = list(
_mood_scrobbles(user, period).order_by("timestamp")
)
if not scrobbles:
return {"streaks": [], "current_streak": None}
streaks = []
current_start = scrobbles[0].timestamp.date()
current_type = scrobbles[0].log.get("mood_type") or "unknown"
current_length = 1
for s in scrobbles[1:]:
mood_type = s.log.get("mood_type") or "unknown"
if mood_type == current_type:
current_length += 1
else:
streaks.append(
{
"start_date": current_start.isoformat(),
"end_date": scrobbles[scrobbles.index(s) - 1].timestamp.date().isoformat(),
"mood_type": current_type,
"length": current_length,
}
)
current_start = s.timestamp.date()
current_type = mood_type
current_length = 1
streaks.append(
{
"start_date": current_start.isoformat(),
"end_date": scrobbles[-1].timestamp.date().isoformat(),
"mood_type": current_type,
"length": current_length,
}
)
streaks.sort(key=lambda x: x["length"], reverse=True)
current_streak = {
"mood_type": current_type,
"length": current_length,
"start_date": current_start.isoformat(),
}
return {"streaks": streaks[:10], "current_streak": current_streak}
def compute_mood_weather(user, period="last_30"):
scrobbles = _mood_scrobbles(user, period)
by_condition = defaultdict(list)
by_temp_range = defaultdict(list)
for s in scrobbles:
quality = _parse_quality(s.log.get("mood_quality"))
if quality is None:
continue
desc = s.log.get("weather_description")
temp = s.log.get("weather_temp")
if desc:
by_condition[desc].append(quality)
if temp is not None:
try:
temp_f = float(temp)
except (TypeError, ValueError):
continue
bucket = f"{(int(temp_f) // 10) * 10}-{(int(temp_f) // 10) * 10 + 9}F"
by_temp_range[bucket].append(quality)
conditions = [
{"condition": cond, "avg_quality": _avg_quality(vals), "count": len(vals)}
for cond, vals in sorted(by_condition.items(), key=lambda x: len(x[1]), reverse=True)
]
temp_ranges = [
{"range": rng, "avg_quality": _avg_quality(vals), "count": len(vals)}
for rng, vals in sorted(by_temp_range.items())
]
return {"conditions": conditions, "temp_ranges": temp_ranges}

View File

@ -10,7 +10,6 @@ PERIOD_DAYS = {
"last_30": 30,
"last_90": 90,
"last_year": 365,
"all_time": None,
}
PERIOD_LABELS = dict(PERIOD_CHOICES)
@ -19,8 +18,15 @@ TIME_BOUND_TRENDS = {
"activity-distribution",
"concurrent-reading",
"concurrent-listening",
"mood-by-time",
"mood-distribution",
"mood-streaks",
"mood-trajectory",
"mood-weather",
"peak-hours",
"reading-pace-vs-activity",
"trending-up",
"weekly-rhythm",
}
TREND_PERIOD_OVERRIDES = {
@ -32,9 +38,7 @@ def get_supported_periods(trend_slug):
if trend_slug in TREND_PERIOD_OVERRIDES:
slugs = TREND_PERIOD_OVERRIDES[trend_slug]
return {s: PERIOD_LABELS[s] for s in slugs}
if trend_slug in TIME_BOUND_TRENDS:
return dict(PERIOD_LABELS)
return {"all_time": PERIOD_LABELS["all_time"]}
return dict(PERIOD_LABELS)
def get_period_days(period):
@ -61,7 +65,7 @@ def get_period_nav(current_period, trend_slug):
return prev_period, next_period
def compute_and_save_trend(user, slug, period="all_time"):
def compute_and_save_trend(user, slug, period="last_30"):
"""Compute a single trend for a given period and persist the result.
Returns elapsed seconds on success, raises on failure.

View File

@ -20,6 +20,31 @@ TREND_METADATA = {
"description": "What music did you listen to while reading books?",
"icon": "📖",
},
"mood-trajectory": {
"title": "Mood Trajectory",
"description": "How your mood quality has changed over time.",
"icon": "📈",
},
"mood-by-time": {
"title": "Mood by Time",
"description": "How your mood varies by hour of day and day of week.",
"icon": "🕐",
},
"mood-distribution": {
"title": "Mood Distribution",
"description": "Which moods you feel most often.",
"icon": "🎭",
},
"mood-streaks": {
"title": "Mood Streaks",
"description": "Your longest runs of positive and negative moods.",
"icon": "🔥",
},
"mood-weather": {
"title": "Mood & Weather",
"description": "How weather conditions correlate with your mood.",
"icon": "🌤",
},
"peak-hours": {
"title": "Peak Activity Hours",
"description": "What time of day are you most active?",
@ -86,7 +111,7 @@ class TrendDetailView(LoginRequiredMixin, TemplateView):
ctx["trend_not_found"] = True
return ctx
period = self.request.GET.get("period", "all_time")
period = self.request.GET.get("period", "last_30")
meta = TREND_METADATA.get(slug, {})
ctx["trend"] = {

View File

@ -1,7 +1,7 @@
import logging
from django.core.management.base import BaseCommand
from django.db import transaction
from django.db import models, transaction
logger = logging.getLogger(__name__)
@ -25,6 +25,11 @@ class Command(BaseCommand):
type=str,
help="Only process series with this imdb_id",
)
parser.add_argument(
"--needs-metadata",
action="store_true",
help="Only process series missing imdb_id or cover image",
)
def handle(self, *args, **options):
from videos.models import Series
@ -32,18 +37,30 @@ class Command(BaseCommand):
force = options["force"]
dry_run = options["dry_run"]
imdb_id = options["imdb_id"]
needs_metadata = options["needs_metadata"]
qs = Series.objects.all()
if imdb_id:
qs = qs.filter(imdb_id=imdb_id)
if needs_metadata:
qs = qs.filter(
models.Q(imdb_id__isnull=True)
| models.Q(imdb_id="")
| models.Q(cover_image__isnull=True)
| models.Q(cover_image="")
)
total = qs.count()
self.stdout.write(f"Processing {total} series")
if dry_run:
for series in qs.iterator():
has_imdb = bool(series.imdb_id)
has_image = bool(series.cover_image)
self.stdout.write(
f" [DRY RUN] Would fix {series.name} (imdb_id={series.imdb_id})"
f" [DRY RUN] Would fix {series.name}"
f" (imdb_id={'' if has_imdb else ''}"
f", image={'' if has_image else ''})"
)
return

View File

@ -321,13 +321,27 @@ class Series(TimeStampedModel):
return not last_scrobble.played_to_completion
def fix_metadata(self, force_update=False):
name_or_id = self.name
if self.imdb_id:
name_or_id = self.imdb_id
video_metadata: VideoMetadata = lookup_video_from_tmdb(name_or_id)
from tmdbv3api import TV
if not video_metadata.title:
logger.warning(f"No imdb data for {self}")
if not self.imdb_id:
tv = TV()
results = tv.search(self.name)
if results:
show_id = results[0].id
external_ids = tv.external_ids(show_id)
if external_ids and external_ids.imdb_id:
self.imdb_id = external_ids.imdb_id
self.save(update_fields=["imdb_id"])
else:
logger.warning(f"No IMDB ID found on TMDB for {self}")
return
else:
logger.warning(f"No results on TMDB for {self.name}")
return
video_metadata = lookup_video_from_tmdb(self.imdb_id)
if not video_metadata or not video_metadata.title:
logger.warning(f"No metadata for {self}")
return
if video_metadata.cover_url and (not self.cover_image or force_update):
@ -336,8 +350,8 @@ class Series(TimeStampedModel):
fname = f"{self.name}_{self.uuid}.jpg"
self.cover_image.save(fname, ContentFile(r.content), save=True)
self.plot = video_metadata.plot
self.imdb_rating = video_metadata.imdb_rating
self.plot = video_metadata.plot or ""
self.imdb_rating = getattr(video_metadata, "imdb_rating", None)
self.save()
if video_metadata.genres:

View File

@ -1,4 +1,5 @@
import logging
import os
import pendulum
from django.conf import settings
@ -8,6 +9,8 @@ from videos.metadata import VideoMetadata, VideoType
TMDB_KEY = getattr(settings, "TMDB_API_KEY", "")
os.environ.setdefault("TMDB_API_KEY", TMDB_KEY)
tmdb = TMDb(key=TMDB_KEY, language="en-US", region="US")
TMDB_IMAGE_URL = "https://image.tmdb.org/t/p/original"
@ -43,6 +46,28 @@ def lookup_video_from_tmdb(name_or_id: str, kind: str = "movie") -> VideoMetadat
) # TODO: enrich this with TMDB url
video_metadata.year = pendulum.parse(media.release_date).year
video_metadata.genres = [g.get("name", "") for g in media.genres]
video_metadata.tmdb_id = media.id
video_metadata.base_run_time_seconds = media.runtime * 60
video_metadata.plot = media.overview
video_metadata.overview = media.overview
video_metadata.tmdb_rating = media.vote_average
if len(tmdb_result.tv_results) > 0:
media = TV().details(tmdb_result.tv_results[0].id)
video_metadata.video_type = VideoType.TV_EPISODE.value
video_metadata.title = media.name
video_metadata.cover_url = (
TMDB_IMAGE_URL + media.poster_path
)
video_metadata.year = pendulum.parse(media.first_air_date).year if media.first_air_date else None
video_metadata.genres = [g.get("name", "") for g in media.genres]
video_metadata.tmdb_id = media.id
video_metadata.base_run_time_seconds = (
media.episode_run_time[0] * 60 if media.episode_run_time else 1800
)
video_metadata.plot = media.overview
video_metadata.overview = media.overview
video_metadata.tmdb_rating = media.vote_average
if len(tmdb_result.tv_episode_results) > 0:
video_metadata.video_type = VideoType.TV_EPISODE.value
@ -63,15 +88,12 @@ def lookup_video_from_tmdb(name_or_id: str, kind: str = "movie") -> VideoMetadat
series.save()
video_metadata.tv_series_id = series.id
video_metadata.tmdb_id = media.id
video_metadata.plot = media.overview
video_metadata.overview = media.overview
if not media:
logger.warning("Video not found on TMDB", extra={"imdb_id": imdb_id})
return video_metadata
video_metadata.tmdb_id = media.id
video_metadata.base_run_time_seconds = media.runtime * 60
video_metadata.plot = media.overview
video_metadata.overview = media.overview
video_metadata.tmdb_rating = media.vote_average
# video_metadata.next_imdb_id = imdb_result.get("next episode", None)
return video_metadata