Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Import: add todomovies #618

Merged
merged 5 commits into from
Sep 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions server/import.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 changes: 18 additions & 0 deletions server/tags.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 == "" {
Expand Down
8 changes: 8 additions & 0 deletions src/lib/Icon.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,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
Expand Down
134 changes: 129 additions & 5 deletions src/routes/(app)/import/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@
MovaryRatings,
MovaryWatchlist,
Watched,
WatchedStatus
WatchedStatus,
TodoMoviesExport,
TagAddRequest,
TodoMoviesCustomList,
TodoMoviesMovie
} from "@/types";
import Icon from "@/lib/Icon.svelte";

Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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"
Expand All @@ -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>
Expand Down
24 changes: 24 additions & 0 deletions src/routes/(app)/import/process/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -414,6 +427,7 @@
<th>Name</th>
<th>Year</th>
<th>Type</th>
<th>Status</th>
{#if !isImporting}
<th></th>
{/if}
Expand Down Expand Up @@ -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
Expand All @@ -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}
Expand Down
5 changes: 4 additions & 1 deletion src/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>();
Expand Down
44 changes: 44 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export type Icon =
| "ryot"
| "trakt"
| "myanimelist"
| "todomovies"
| "themoviedb"
| "refresh"
| "gamepad"
Expand Down Expand Up @@ -869,6 +870,7 @@ export interface ImportedList {
activity?: Activity[];
watchedEpisodes?: WatchedEpisode[];
watchedSeasons?: WatchedSeason[];
tags?: TagAddRequest[];
}

export interface Filters {
Expand Down Expand Up @@ -1049,6 +1051,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;
Expand Down