mirror of
https://github.com/sbondCo/Watcharr.git
synced 2026-08-07 07:14:44 +00:00
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 == "" {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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>();
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user