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

Ryot import implementation #563

Merged
merged 11 commits into from
Jun 28, 2024
37 changes: 37 additions & 0 deletions src/lib/Icon.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,43 @@
/>
</g>
</svg>
{:else if i === "ryot"}
<svg
xmlns="http://www.w3.org/2000/svg"
width={wh}
height={wh}
viewBox="0 0 120 120"
preserveAspectRatio="xMidYMid meet"
>
<g transform="translate(0.000000,120.000000) scale(0.0500000,-0.0500000)" fill="currentColor">
<path
d="M1126 2369 c-402 -59 -745 -340 -881 -722 -167 -469 1 -995 407
-1275 518 -356 1213 -220 1555 305 199 304 233 689 93 1026 -96 232 -297 445
-527 561 -192 96 -436 136 -647 105z m221 -131 c-3 -13 -40 -241 -83 -508 -42
-267 -80 -489 -84 -493 -7 -8 -530 775 -530 793 0 12 95 78 175 121 134 72
272 107 429 108 97 1 98 1 93 -21z m302 -44 c20 -9 32 -18 26 -19 -5 -2 -35
-10 -65 -19 -30 -8 -81 -31 -112 -50 -66 -39 -65 -40 -48 60 16 90 10 85 91
63 39 -11 88 -27 108 -35z m255 -173 c128 -63 199 -178 200 -326 1 -70 -3 -89
-31 -147 -134 -284 -529 -284 -661 0 -22 48 -27 71 -27 147 0 77 4 98 26 146
68 144 195 222 351 216 62 -3 89 -10 142 -36z m-1278 -173 c33 -50 62 -97 66
-104 6 -9 -8 -10 -55 -7 -72 6 -149 -8 -209 -37 -23 -11 -43 -18 -45 -17 -1 2
19 40 45 85 47 82 116 172 131 172 5 0 35 -41 67 -92z m109 -252 c110 -52 178
-160 179 -282 1 -62 -3 -80 -34 -140 -56 -110 -137 -160 -281 -173 l-66 -6 67
-240 c36 -132 65 -242 63 -244 -9 -11 -150 142 -200 217 -95 142 -148 295
-163 467 -14 174 13 270 101 349 62 56 114 74 209 75 59 1 86 -4 125 -23z
m1516 -129 c19 -96 26 -281 10 -306 -7 -11 -89 -12 -491 -6 l-482 8 5 26 c3
14 13 76 23 138 l18 112 57 -63 c150 -164 379 -202 583 -99 97 49 205 185 230
288 7 26 9 28 18 12 5 -10 18 -60 29 -110z m-1154 -332 l63 -95 95 -362 c53
-200 98 -369 101 -377 4 -12 -16 -13 -118 -8 -141 6 -249 31 -353 81 l-70 33
-64 234 c-35 129 -65 238 -65 242 -1 4 22 16 49 25 74 26 136 65 184 117 42
46 101 155 101 188 0 32 15 17 77 -78z m619 -99 c1 0 4 -146 7 -323 l6 -321
-47 -20 c-62 -27 -200 -67 -207 -61 -4 4 -195 719 -195 731 0 3 434 -2 436 -6z
m519 -7 c9 -10 -49 -162 -90 -235 -52 -93 -145 -205 -222 -269 -37 -30 -71
-55 -75 -55 -4 0 -8 64 -8 143 0 78 -3 207 -7 285 l-6 144 201 -4 c110 -1 203
-6 207 -9z"
/>
</g>
</svg>
{:else if i === "gamepad"}
<svg xmlns="http://www.w3.org/2000/svg" width={wh} height={wh} viewBox="0 0 512 512">
<path
Expand Down
5 changes: 1 addition & 4 deletions src/lib/stats/Stat.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,13 @@
> span:first-of-type {
font-weight: bold;
font-size: 20px;
margin-top: auto;

&.large {
font-size: 32px;
}
}

> span:last-child {
margin-top: auto;
}

.disclaimer {
position: absolute;
top: 5px;
Expand Down
126 changes: 125 additions & 1 deletion src/routes/(app)/import/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
MovaryHistory,
MovaryRatings,
MovaryWatchlist,
Watched
Watched,
WatchedStatus
} from "@/types";

let isDragOver = false;
Expand Down Expand Up @@ -369,6 +370,127 @@
}
}

async function processRyotFile(files?: FileList | null) {
try {
console.log("processRyotFile", files);
if (!files || files?.length <= 0) {
console.error("processRyotFile", "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 !== "application/json") {
notify({
type: "error",
text: "Must be a Ryot JSON export file"
});
isLoading = false;
isDragOver = false;
return;
}

// Build toImport array
const toImport: ImportedList[] = [];
const fileText = await readFile(new FileReader(), file);
const jsonData = JSON.parse(fileText)["media"] as any[];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed to any array, the data in the ryot export didnt conform to the Watched[] type

for (const v of jsonData) {
if (!v.source_id || !v.identifier || !(v.lot == "show" || v.lot == "movie")) {
notify({
type: "error",
text: "Item in export either has no title, TMDB identifier or is not a movie/tv show! Look in console for more details."
});
console.error(
"Can't add export item to import table! It has title, TMDB identifier or is not a movie/tv show! Item:",
v
);
continue;
}

// Define the main general status of the movie/show
// In Ryot, it can be marked as multiple of the following, so choose the most relevant
const statusRanks: [string, WatchedStatus][] = [
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gave this a "tuple array" type so that it knows the second value is a WatchedStatus, fixed a type error below where we add status to the ImportedList object.

["", "DROPPED"],
["Watchlist", "PLANNED"],
["Monitoring", "PLANNED"],
["In Progress", "WATCHING"],
["Completed", "FINISHED"]
];
let rank = 0;
for (const s of v.collections) {
rank = Math.max(
rank,
statusRanks.findIndex((pair) => pair[0] == s)
);
}

const t: ImportedList = {
tmdbId: Number(v.identifier),
name: v.source_id,
type: v.lot === "show" ? "tv" : v.lot,
status: statusRanks[rank][1],

// In Ryot, shows can have one review for each episode - Not supported in Watcharr
// Will ignore the episodes' reviews
thoughts: v.lot === "movie" && v.reviews.length ? v.reviews[0].review.text : "",

// Ryot does not support overall rating for shows
rating: v.lot === "movie" && v.reviews.length ? Number(v.reviews[0].rating) : undefined,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed null usage to undefined to conform to the rating property type.

Irrelevant, but maybe informative if you care:
I usually try to avoid using null and use undefined instead. Since JS has null and undefined, they usually tell us the same thing, so I prefer to, in most cases, strictly use undefined instead (can't think of any upsides to null other than if our logic needs to differentiate between the two for some reason). Anyways that's just what I think.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yeah, that makes sense.

JS is not my main language, so I was going with knowledge from a few years ago when I decided on null.
I will keep that in mind for future PRs


datesWatched:
v.lot === "movie" && v.seen_history.length
? v.seen_history.map((seen: any) => new Date(seen.ended_on))
: [],

// Episode ratings are on a separate field: "reviews"
watchedEpisodes: v.seen_history.map((episode: any) => ({
status: episode.progress === "100" ? "FINISHED" : "WATCHING",

// Linear :( search the reviews for a match
rating:
Number(
(
v.reviews.find(
(review: any) =>
review.show_season_number === episode.show_season_number &&
review.show_episode_number === episode.show_episode_number
) || {}
).rating
) || null,

seasonNumber: episode.show_season_number,
episodeNumber: episode.show_episode_number
}))
};
toImport.push(t);
}
console.log("toImport:", toImport);
importedList.set({
data: JSON.stringify(toImport),
type: "ryot"
});
goto("/import/process");
} catch (err) {
isLoading = false;
notify({ type: "error", text: "Failed to read file!" });
console.error("import: Failed to read file!", err);
}
}

onMount(() => {
if (!localStorage.getItem("token")) {
goto("/login");
Expand Down Expand Up @@ -407,6 +529,8 @@
text="MyAnimeList Export"
filesSelected={(f) => processFilesMyAnimeList(f)}
/>

<DropFileButton icon="ryot" text="Ryot Exports" filesSelected={(f) => processRyotFile(f)} />
{/if}
</div>
</div>
Expand Down
13 changes: 13 additions & 0 deletions src/routes/(app)/import/process/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,19 @@
text: "Failed to process import data!"
});
}
} else if (list?.type === "ryot") {
importText = "Ryot";
try {
const s = JSON.parse(list.data);
// Builds imported list in previous step for ease.
rList = s;
} catch (err) {
console.error("Ryot 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
2 changes: 1 addition & 1 deletion src/routes/(app)/movie/[id]/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@
/>

<span class="quick-info">
<span>{movie.runtime}m</span>
<span>{movie.runtime} min</span>

<div>
{#each movie.genres as g, i}
Expand Down
30 changes: 23 additions & 7 deletions src/routes/(app)/profile/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -134,12 +134,28 @@

/**
* Takes in number of minutes and converts to readable.
* eg into hours and minutes.
* eg into months, weeks, days, hours and minutes.
*/
function toFormattedMinutes(m: number) {
const hours = Math.floor(m / 60);
const minutes = m % 60;
return `${hours ? `${hours}h ` : ""}${minutes}m`;
function toFormattedTimeLong(m: number) {
// Considers a 30 days long month
const countInMinutes = [
["month", 43200],
["week", 10080],
["day", 1440],
["hour", 60]
];

let ansString = "";
let tmp;
for (const c of countInMinutes) {
tmp = Math.floor(m / (c[1] as number));

// Ignore fields with fewer than 1 unit
if (tmp) ansString += `${tmp} ${c[0]}${tmp >= 2 ? "s, " : ", "}`;
m -= tmp * (c[1] as number);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Casted these to be numbers, I could've done the tuple array thing here too but i didn't think of that until now and its too late!!!!!! AAAAAAA

}

return ansString.slice(0, -2);
}
</script>

Expand All @@ -163,10 +179,10 @@
<Stat name="Joined" value={formatDate(new Date(profile.joined))} />
<Stat name="Movies Watched" value={profile.moviesWatched} large />
<Stat name="Shows Watched" value={profile.showsWatched} large />
<Stat name="Watching Movies" value={toFormattedMinutes(profile.moviesWatchedRuntime)} />
<Stat name="Watching Movies" value={toFormattedTimeLong(profile.moviesWatchedRuntime)} />
<Stat
name="Watching Shows"
value={toFormattedMinutes(profile.showsWatchedRuntime)}
value={toFormattedTimeLong(profile.showsWatchedRuntime)}
disc="This is very inaccurate 🚀"
/>
{:catch err}
Expand Down
2 changes: 1 addition & 1 deletion src/routes/(app)/tv/[id]/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@

<span class="quick-info">
{#if show?.episode_run_time?.length > 0}
<span>{show.episode_run_time.join(",")}m</span>
<span>{show.episode_run_time.join(",")} min</span>
{/if}

<div>
Expand Down
3 changes: 2 additions & 1 deletion src/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ 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" } | undefined
| { data: string; type: "text-list" | "tmdb" | "movary" | "watcharr" | "myanimelist" | "ryot" }
| undefined
>();
export const parsedImportedList = writable<ImportedList[] | undefined>();
export const searchQuery = writable<string>("");
Expand Down
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export type Icon =
| "eye"
| "star"
| "movary"
| "ryot"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added ryot to the Icon type (every icons name is included in this type).

| "refresh"
| "gamepad"
| "film"
Expand Down