Merge pull request #618 from AlexPerathoner/dev

Import: add todomovies
This commit is contained in:
Mr
2024-09-16 23:52:38 +00:00
committed by GitHub
7 changed files with 262 additions and 6 deletions
+35
View File
@@ -38,6 +38,7 @@ type ImportRequest struct {
Activity []Activity `json:"activity"`
WatchedEpisodes []WatchedEpisode `json:"watchedEpisodes"`
WatchedSeason []WatchedSeason `json:"watchedSeasons"`
Tags []TagAddRequest `json:"tags"`
}
type ImportResponse struct {
@@ -228,5 +229,39 @@ func successfulImport(db *gorm.DB, userId uint, contentId int, contentType Conte
w.WatchedEpisodes = ws.WatchedEpisodes
}
}
// Import tags, if any
if len(ar.Tags) > 0 {
// Create tags if they dont exist
slog.Debug("successfulImport: Importing tags")
for _, v := range ar.Tags {
// Check if tag exists
var t Tag
t, err := getTagByNameAndColor(db, userId, v.Name, v.Color, v.BgColor)
if err != nil && err.Error() != "tag does not exist" {
slog.Error("successfulImport: Failed to check for an existing tag", "name", v.Name, "error", err)
continue
}
if t.ID == 0 {
tag, err := addTag(db, userId, TagAddRequest{
Name: v.Name,
Color: v.Color,
BgColor: v.BgColor,
})
if err != nil {
slog.Error("successfulImport: Failed to add a tag.", "name", v.Name, "error", err)
continue
}
t = tag
}
// Associate the watched entry with the tag
err = addWatchedToTag(db, userId, t.ID, w.ID)
if err != nil {
slog.Error("successfulImport: Failed to associate watched entry with tag.", "error", err)
continue
}
w.Tags = append(w.Tags, t)
}
}
return ImportResponse{Type: IMPORT_SUCCESS, WatchedEntry: w}, nil
}
+18
View File
@@ -54,6 +54,24 @@ func getTags(db *gorm.DB, userId uint) ([]Tag, error) {
// return *tag, nil
// }
// This method should only be used when we don't have the tagId
// (eg: when we are importing data) because this is not technically
// reliable, since users can have multiple tags with the same name/colors
// (realistically they probably won't, but...).
func getTagByNameAndColor(db *gorm.DB, userId uint, tagName string, tagColor string, tagBgColor string) (Tag, error) {
tag := new(Tag)
res := db.Model(&Tag{}).Where("name = ? AND user_id = ? AND color = ? AND bg_color = ?", tagName, userId, tagColor, tagBgColor).Preload("Watched").Find(&tag)
if res.Error != nil {
slog.Error("getTagByNameAndColor: Failed getting tag from database", "error", res.Error.Error())
return Tag{}, errors.New("failed getting tag")
}
if tag.ID == 0 {
slog.Error("getTagByNameAndColor: Tag does not exist for this user.", "user_id", userId)
return Tag{}, errors.New("tag does not exist")
}
return *tag, nil
}
// Let user create a tag.
func addTag(db *gorm.DB, userId uint, tr TagAddRequest) (Tag, error) {
if tr.Name == "" {
+8
View File
@@ -357,6 +357,14 @@
d="M8.273 7.247v8.423l-2.103-.003v-5.216l-2.03 2.404-1.989-2.458-.02 5.285H.001L0 7.247h2.203l1.865 2.545 2.015-2.546 2.19.001zm8.628 2.069l.025 6.335h-2.365l-.008-2.871h-2.8c.07.499.21 1.266.417 1.779.155.381.298.751.583 1.128l-1.705 1.125c-.349-.636-.622-1.337-.878-2.082a9.296 9.296 0 0 1-.507-2.179c-.085-.75-.097-1.471.107-2.212a3.908 3.908 0 0 1 1.161-1.866c.313-.293.749-.5 1.1-.687.351-.187.743-.264 1.107-.359a7.405 7.405 0 0 1 1.191-.183c.398-.034 1.107-.066 2.39-.028l.545 1.749H14.51c-.593.008-.878.001-1.341.209a2.236 2.236 0 0 0-1.278 1.92l2.663.033.038-1.81h2.309zm3.992-2.099v6.627l3.107.032-.43 1.775h-4.807V7.187l2.13.03z"
/>
</svg>
{:else if i === "todomovies"}
<svg xmlns="http://www.w3.org/2000/svg" width={wh} height={wh} viewBox="100 20 600 570">
<path d="m494.113 203.464 50.423 50.423-178.649 178.65-50.423-50.424z" />
<path d="m234.964 300.388 50.424-50.424 130.149 130.15-50.424 50.423z" />
<path
d="m95.774 15.493 187.325.704c.704 0-5.634 276.761-188.733 488.029M700.705 14.789l-187.243.704c-.703 0 5.632 276.76 188.651 488.029"
/>
</svg>
{:else if i === "themoviedb"}
<svg xmlns="http://www.w3.org/2000/svg" width={wh} height={wh} viewBox="0 0 24 24">
<path
+129 -5
View File
@@ -19,7 +19,11 @@
MovaryRatings,
MovaryWatchlist,
Watched,
WatchedStatus
WatchedStatus,
TodoMoviesExport,
TagAddRequest,
TodoMoviesCustomList,
TodoMoviesMovie
} from "@/types";
import Icon from "@/lib/Icon.svelte";
@@ -510,6 +514,120 @@
}
}
async function processTodoMoviesFile(files?: FileList | null) {
try {
console.log("processFilesTodoMovies", files);
if (!files || files?.length <= 0) {
console.error("processFilesTodoMovies", "No files to process!");
notify({
type: "error",
text: "File not found in dropped items. Please try again or refresh.",
time: 6000
});
isDragOver = false;
return;
}
isLoading = true;
if (files.length > 1) {
notify({
type: "error",
text: "Only one file at a time is supported. Continuing with the first.",
time: 6000
});
}
// Currently only support for importing one file at a time
const file = files[0];
if (file.type !== "" && !file.name.endsWith(".todomovieslist")) {
notify({
type: "error",
text: "Must be a TodoMovies backup file (.todomovieslist)"
});
isLoading = false;
isDragOver = false;
return;
}
// Read file data into strings
let exportTodoMoviesStr: string | undefined;
const r = new FileReader();
exportTodoMoviesStr = await readFile(r, file);
if (!exportTodoMoviesStr) {
notify({
type: "error",
text: "Failed to read export file. Ensure you have attached the correct file.",
time: 6000
});
isDragOver = false;
isLoading = false;
return;
}
console.log("Loaded file");
const exportTodoMovies: TodoMoviesExport = JSON.parse(exportTodoMoviesStr);
console.log("exportTodoMovies:", exportTodoMovies);
const movieList: TodoMoviesMovie[] = exportTodoMovies.Movie;
const customLists: TodoMoviesCustomList[] = exportTodoMovies.MovieList;
// Build toImport array
const toImport: ImportedList[] = [];
// Convert the timestamp to seconds (NSDate uses seconds since 2001-01-01 00:00:00 UTC)
const referenceDate = new Date(2001, 0, 1);
// Add all history movies. There is only one entry for every movie.
// Movies which have already been watched but which have been added back in planning are also included, as this could also have ratings and comments.
for (let i = 0; i < movieList.length; i++) {
const h = movieList[i];
// Skip if no tmdb id.
if (!h.Attrs.tmdbID) {
continue;
}
// Skip if already added. The first time it is added we get all info needed from other entries.
if (toImport.filter((ti) => ti.tmdbId == Number(h.Attrs.tmdbID)).length > 0) {
continue;
}
const nsInsertionDate = h.Attrs.insertionDate.Value;
const date = new Date(referenceDate.getTime() + nsInsertionDate * 1000);
const tagsIds = h.Rels.lists.Items;
const tags = tagsIds.map((tagId) => {
const tag = customLists.find((list) => list.ObjectID == tagId);
return {
name: "TodoMovies list: " + tag?.Attrs.name,
color: "#000000",
bgColor: tag?.Attrs.colorInHex
} as TagAddRequest;
});
const t: ImportedList = {
name: h.Attrs.title,
tmdbId: Number(h.Attrs.tmdbID),
status: h.Attrs.isWatched == 1 ? "FINISHED" : "PLANNED",
type: "movie", // TodoMovies only supports movies
datesWatched: [date], // use activities instead
thoughts: "", // no comments in TodoMovies
rating: h.Attrs.myScore,
tags: tags
};
toImport.push(t);
}
console.log("toImport:", toImport);
importedList.set({
data: JSON.stringify(toImport),
type: "todomovies"
});
goto("/import/process");
} catch (err) {
isLoading = false;
notify({ type: "error", text: "Failed to read files!" });
console.error("import: Failed to read files!", err);
}
}
onMount(() => {
if (!localStorage.getItem("token")) {
goto("/login");
@@ -540,6 +658,11 @@
filesSelected={(f) => processFiles(f)}
/>
<button class="plain" on:click={() => goto("/import/trakt")}>
<Icon i="trakt" wh="100%" />
<h4 class="norm">Trakt Import</h4>
</button>
<DropFileButton
icon="movary"
text="Movary Exports"
@@ -555,10 +678,11 @@
<DropFileButton icon="ryot" text="Ryot Exports" filesSelected={(f) => processRyotFile(f)} />
<button class="plain" on:click={() => goto("/import/trakt")}>
<Icon i="trakt" wh="100%" />
<h4 class="norm">Trakt Import</h4>
</button>
<DropFileButton
icon="todomovies"
text="TodoMovies"
filesSelected={(f) => processTodoMoviesFile(f)}
/>
{/if}
</div>
</div>
@@ -266,6 +266,19 @@
text: "Processing failed!. Please report this issue if it persists."
});
}
} else if (list?.type === "todomovies") {
importText = "TodoMovies";
try {
const s = JSON.parse(list.data);
// Builds imported list in previous step for ease.
rList = s;
} catch (err) {
console.error("TodoMovies import processing failed!", err);
notify({
type: "error",
text: "Processing failed!. Please report this issue if it persists."
});
}
}
// TODO: remove duplicate names in list
return list;
@@ -414,6 +427,7 @@
<th>Name</th>
<th>Year</th>
<th>Type</th>
<th>Status</th>
{#if !isImporting}
<th></th>
{/if}
@@ -456,6 +470,15 @@
disabled={isImporting}
/>
</td>
<td class="type">
<DropDown
options={["FINISHED", "PLANNED", "WATCHING", "HOLD", "DROPPED"]}
bind:active={l.status}
placeholder="Status"
blendIn={true}
disabled={isImporting}
/>
</td>
{#if !isImporting}
<td>
<button
@@ -477,6 +500,7 @@
<input class="plain" id="addYear" placeholder="YYYY" type="number" />
</td>
<td class="type"></td>
<td class="status"></td>
<td></td>
</tr>
{/if}
+4 -1
View File
@@ -25,7 +25,10 @@ export const activeSort = writable<string[]>(defaultSort);
export const activeFilters = writable<Filters>({ type: [], status: [] });
export const appTheme = writable<Theme>();
export const importedList = writable<
| { data: string; type: "text-list" | "tmdb" | "movary" | "watcharr" | "myanimelist" | "ryot" }
| {
data: string;
type: "text-list" | "tmdb" | "movary" | "watcharr" | "myanimelist" | "ryot" | "todomovies";
}
| undefined
>();
export const parsedImportedList = writable<ImportedList[] | undefined>();
+44
View File
@@ -38,6 +38,7 @@ export type Icon =
| "ryot"
| "trakt"
| "myanimelist"
| "todomovies"
| "themoviedb"
| "refresh"
| "gamepad"
@@ -896,6 +897,7 @@ export interface ImportedList {
activity?: Activity[];
watchedEpisodes?: WatchedEpisode[];
watchedSeasons?: WatchedSeason[];
tags?: TagAddRequest[];
}
export interface Filters {
@@ -1076,6 +1078,48 @@ export interface MovaryWatchlist extends MovaryExportBase {
addedAt: string;
}
export interface TodoMoviesExport {
Movie: TodoMoviesMovie[];
MovieList: TodoMoviesCustomList[];
}
export interface TodoMoviesMovie {
Attrs: {
tmdbID: number;
title: string;
isWatched: number;
insertionDate: {
Value: number;
Class: string;
};
myScore: number;
};
Rels: {
lists: {
Items: string[];
Entity: string;
};
};
ObjectID: string;
}
export interface TodoMoviesCustomList {
Attrs: {
colorInHex: string;
order: number;
iconFileName: string;
featuredListID: number;
name: string;
};
Rels: {
movies: {
Items: string[];
Entity: string;
};
};
ObjectID: string;
}
export interface Game {
id: number;
updatedAt: string;