feat: include title in watchlist csv export

This commit is contained in:
2026-05-07 00:06:18 +02:00
parent 5ee16a19ef
commit 43de6544fe
3 changed files with 27 additions and 12 deletions

View File

@@ -210,23 +210,36 @@ func (s *Service) ImportWatchlist(ctx context.Context, userID string, r io.Reade
} }
for i, record := range records { for i, record := range records {
if len(record) < 4 { // New format: anime_id,title,status,current_episode,current_time_seconds
log.Printf("skipping row %d: insufficient columns", i+2) // i+2 because i is 0-indexed record after header // Old format: anime_id,status,current_episode,current_time_seconds
var animeIDStr, status, episodeStr, timeStr string
if len(record) >= 5 {
animeIDStr = record[0]
status = record[2]
episodeStr = record[3]
timeStr = record[4]
} else if len(record) >= 4 {
animeIDStr = record[0]
status = record[1]
episodeStr = record[2]
timeStr = record[3]
} else {
log.Printf("skipping row %d: insufficient columns", i+2)
continue continue
} }
animeID, err := strconv.ParseInt(record[0], 10, 64) animeID, err := strconv.ParseInt(animeIDStr, 10, 64)
if err != nil { if err != nil {
return fmt.Errorf("row %d: invalid anime id: %w", i+2, err) return fmt.Errorf("row %d: invalid anime id: %w", i+2, err)
} }
status := record[1]
if _, ok := validStatuses[status]; !ok { if _, ok := validStatuses[status]; !ok {
status = "plan_to_watch" status = "plan_to_watch"
} }
currentEpisode, _ := strconv.ParseInt(record[2], 10, 64) currentEpisode, _ := strconv.ParseInt(episodeStr, 10, 64)
currentTimeSeconds, _ := strconv.ParseFloat(record[3], 64) currentTimeSeconds, _ := strconv.ParseFloat(timeStr, 64)
if err := s.ensureAnimeExists(ctx, animeID); err != nil { if err := s.ensureAnimeExists(ctx, animeID); err != nil {
return fmt.Errorf("row %d: failed to ensure anime: %w", i+2, err) return fmt.Errorf("row %d: failed to ensure anime: %w", i+2, err)

View File

@@ -59,7 +59,7 @@
<div id="watchlist-items" class="grid grid-cols-2 gap-4 md:grid-cols-3 lg:grid-cols-4 2xl:grid-cols-6"> <div id="watchlist-items" class="grid grid-cols-2 gap-4 md:grid-cols-3 lg:grid-cols-4 2xl:grid-cols-6">
{{range $.AllEntries}} {{range $.AllEntries}}
<div class="watchlist-item flex w-full flex-col gap-2" data-status="{{.Status}}" data-updated-at="{{.UpdatedAt.Unix}}" data-episode="{{.CurrentEpisode.Int64}}" data-time="{{.CurrentTimeSeconds}}"> <div class="watchlist-item flex w-full flex-col gap-2" data-status="{{.Status}}" data-updated-at="{{.UpdatedAt.Unix}}" data-episode="{{.CurrentEpisode.Int64}}" data-time="{{.CurrentTimeSeconds}}" data-title="{{.DisplayTitle}}">
<div class="group relative flex aspect-2/3 w-full flex-col overflow-hidden bg-white/5 after:absolute after:inset-0 after:bg-black/80 after:opacity-0 hover:after:opacity-100 after:transition-opacity"> <div class="group relative flex aspect-2/3 w-full flex-col overflow-hidden bg-white/5 after:absolute after:inset-0 after:bg-black/80 after:opacity-0 hover:after:opacity-100 after:transition-opacity">
<a href="/anime/{{.AnimeID}}" class="absolute inset-0 z-10"></a> <a href="/anime/{{.AnimeID}}" class="absolute inset-0 z-10"></a>
<img src="{{.ImageUrl}}" alt="{{.DisplayTitle}}" class="h-full w-full object-cover" loading="lazy" /> <img src="{{.ImageUrl}}" alt="{{.DisplayTitle}}" class="h-full w-full object-cover" loading="lazy" />
@@ -154,13 +154,14 @@
return; return;
} }
let csv = 'anime_id,status,current_episode,current_time_seconds\n'; let csv = 'anime_id,title,status,current_episode,current_time_seconds\n';
items.forEach(function(item) { items.forEach(function(item) {
const animeId = item.querySelector('a').href.split('/').pop(); const animeId = item.querySelector('a').href.split('/').pop();
const title = (item.dataset.title || '').replace(/"/g, '""');
const status = item.dataset.status || 'plan_to_watch'; const status = item.dataset.status || 'plan_to_watch';
const episode = item.dataset.episode || '0'; const episode = item.dataset.episode || '0';
const time = item.dataset.time || '0'; const time = item.dataset.time || '0';
csv += `${animeId},${status},${episode},${time}\n`; csv += `${animeId},"${title}",${status},${episode},${time}\n`;
}); });
const blob = new Blob([csv], { type: 'text/csv' }); const blob = new Blob([csv], { type: 'text/csv' });

View File

@@ -62,7 +62,7 @@
<div class="watchlist-section" data-status="{{$status}}"> <div class="watchlist-section" data-status="{{$status}}">
<div class="grid grid-cols-2 gap-4 md:grid-cols-3 lg:grid-cols-4 2xl:grid-cols-6"> <div class="grid grid-cols-2 gap-4 md:grid-cols-3 lg:grid-cols-4 2xl:grid-cols-6">
{{range $entries}} {{range $entries}}
<div class="watchlist-item flex w-full flex-col gap-2" data-status="{{.Status}}" data-updated-at="{{.UpdatedAt.Unix}}" data-episode="{{.CurrentEpisode.Int64}}" data-time="{{.CurrentTimeSeconds}}"> <div class="watchlist-item flex w-full flex-col gap-2" data-status="{{.Status}}" data-updated-at="{{.UpdatedAt.Unix}}" data-episode="{{.CurrentEpisode.Int64}}" data-time="{{.CurrentTimeSeconds}}" data-title="{{.DisplayTitle}}">
<div class="group relative flex aspect-2/3 w-full flex-col overflow-hidden bg-white/5 after:absolute after:inset-0 after:bg-black/80 after:opacity-0 hover:after:opacity-100 after:transition-opacity"> <div class="group relative flex aspect-2/3 w-full flex-col overflow-hidden bg-white/5 after:absolute after:inset-0 after:bg-black/80 after:opacity-0 hover:after:opacity-100 after:transition-opacity">
<a href="/anime/{{.AnimeID}}" class="absolute inset-0"></a> <a href="/anime/{{.AnimeID}}" class="absolute inset-0"></a>
<img src="{{.ImageUrl}}" alt="{{.DisplayTitle}}" class="h-full w-full object-cover" loading="lazy" /> <img src="{{.ImageUrl}}" alt="{{.DisplayTitle}}" class="h-full w-full object-cover" loading="lazy" />
@@ -155,13 +155,14 @@
return; return;
} }
let csv = 'anime_id,status,current_episode,current_time_seconds\n'; let csv = 'anime_id,title,status,current_episode,current_time_seconds\n';
items.forEach(function(item) { items.forEach(function(item) {
const animeId = item.querySelector('a').href.split('/').pop(); const animeId = item.querySelector('a').href.split('/').pop();
const title = (item.dataset.title || '').replace(/"/g, '""');
const status = item.dataset.status || 'plan_to_watch'; const status = item.dataset.status || 'plan_to_watch';
const episode = item.dataset.episode || '0'; const episode = item.dataset.episode || '0';
const time = item.dataset.time || '0'; const time = item.dataset.time || '0';
csv += `${animeId},${status},${episode},${time}\n`; csv += `${animeId},"${title}",${status},${episode},${time}\n`;
}); });
const blob = new Blob([csv], { type: 'text/csv' }); const blob = new Blob([csv], { type: 'text/csv' });