feat: added rewind data

Signed-off-by: Varun Raj <varun@skcript.com>
This commit is contained in:
Varun Raj
2024-12-24 20:43:35 +05:30
parent 7266a2edd6
commit 2b7dc94775
5 changed files with 259 additions and 28 deletions
+1 -1
View File
@@ -43,7 +43,7 @@ export const IntroComposition = ({ scenes }: { scenes: Scene[] }) => {
fontFamily: "sans-serif",
}}
>
<Sequence name="Logo Intro" from={0} durationInFrames={screenDuration}>
<Sequence name="Logo Intro" durationInFrames={screenDuration}>
<div style={{
width: "100%",
height: "100%",
+4 -1
View File
@@ -48,4 +48,7 @@ export const HEATMAP_DATA = BASE_API_ENDPOINT + "/analytics/statistics/heatmap";
export const GET_FILTERS = BASE_API_ENDPOINT + "/filters/asset-filters";
// Person
export const GET_PERSON_INFO = (personId: string) => BASE_API_ENDPOINT + "/people/" + personId + "/info";
export const GET_PERSON_INFO = (personId: string) => BASE_API_ENDPOINT + "/people/" + personId + "/info";
// Rewind
export const REWIND_STATS = BASE_API_ENDPOINT + "/rewind/stats";
+6 -2
View File
@@ -1,4 +1,4 @@
import { GET_FILTERS, SEARCH_PLACES_PATH } from "@/config/routes";
import { GET_FILTERS, REWIND_STATS, SEARCH_PLACES_PATH } from "@/config/routes";
import API from "@/lib/api";
import { IPlace } from "@/types/common";
@@ -9,4 +9,8 @@ export const searchPlaces = async (name: string): Promise<IPlace[]> => {
export const getFilters = async () => {
return API.get(GET_FILTERS);
}
}
export const getRewindStats = async () => {
return API.get(REWIND_STATS);
}
+125
View File
@@ -0,0 +1,125 @@
import { NextApiRequest, NextApiResponse } from "next";
import { and, desc, gt, isNotNull, isNull, ne, sql } from "drizzle-orm";
import { eq } from "drizzle-orm";
import { db } from "@/config/db";
import { count } from "drizzle-orm";
import { assets } from "@/schema/assets.schema";
import { assetFaces } from "@/schema/assetFaces.schema";
import { exif, person, users } from "@/schema";
import { getCurrentUser } from "@/handlers/serverUtils/user.utils";
import { albums } from "@/schema/albums.schema";
import { albumsAssetsAssets } from "@/schema/albumAssetsAssets.schema";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== "GET") {
return res.status(405).json({ message: "Method Not Allowed" });
}
const { year = "2024" } = req.query as { year: string };
const user = await getCurrentUser(req);
if (!user) {
return res.status(403).json({ message: "Forbidden" });
}
const numberOfPhotos = await db.select({ count: count(assets.id) }).from(assets).where(and(
eq(assets.ownerId, user.id),
eq(assets.isVisible, true),
eq(assets.isArchived, false),
));
// Get list of countries visited in the year
const countriesInYear = await db
.select({ name: exif.country, count: count(assets.id) })
.from(exif)
.leftJoin(assets, eq(assets.id, exif.assetId))
.where(and(
eq(sql`EXTRACT(YEAR FROM ${exif.dateTimeOriginal})`, year),
eq(assets.ownerId, user.id),
eq(assets.isVisible, true),
eq(assets.isArchived, false),
))
.groupBy(exif.country);
const countries = countriesInYear.filter(country => !!country.name).map(country => country.name);
const countOfCities = await db
.select({ name: exif.city, count: count(assets.id) })
.from(exif)
.leftJoin(assets, eq(assets.id, exif.assetId))
.where(and(
eq(sql`EXTRACT(YEAR FROM ${exif.dateTimeOriginal})`, year),
eq(assets.ownerId, user.id),
eq(assets.isVisible, true),
eq(assets.isArchived, false),
))
.groupBy(exif.city);
const cities = countOfCities.filter(city => !!city.name).map(city => city.name);
const albumsWithMostPhotos = await db
.select({
name: albums.albumName,
count: count(assets.id),
cover: albums.albumThumbnailAssetId
})
.from(albumsAssetsAssets)
.leftJoin(albums, eq(albums.id, albumsAssetsAssets.albumsId))
.leftJoin(assets, eq(assets.id, albumsAssetsAssets.assetsId))
.leftJoin(exif, eq(exif.assetId, assets.id))
.where(and(
eq(sql`EXTRACT(YEAR FROM ${exif.dateTimeOriginal})`, year),
eq(assets.ownerId, user.id),
eq(assets.isVisible, true),
eq(assets.isArchived, false),
))
.orderBy(desc(count(assets.id)))
.limit(4)
.groupBy(albums.id);
const peopleWithMostPhotos = await db
.select({
name: person.name,
count: count(assets.id),
id: person.id,
cover: person.faceAssetId
})
.from(assetFaces)
.leftJoin(assets, eq(assets.id, assetFaces.assetId))
.leftJoin(person, eq(person.id, assetFaces.personId))
.leftJoin(exif, eq(exif.assetId, assets.id))
.where(and(
eq(sql`EXTRACT(YEAR FROM ${exif.dateTimeOriginal})`, year),
eq(assets.ownerId, user.id),
eq(assets.isVisible, true),
eq(assets.isArchived, false),
isNotNull(person.name),
))
.orderBy(desc(count(assets.id)))
.limit(2)
.groupBy(person.id);
const favoritedAssets = await db
.select({ id: assets.id })
.from(assets)
.leftJoin(exif, eq(exif.assetId, assets.id))
.where(and(
eq(assets.isVisible, true),
eq(assets.isFavorite, true),
eq(assets.isArchived, false),
eq(assets.ownerId, user.id),
eq(sql`EXTRACT(YEAR FROM ${exif.dateTimeOriginal})`, year),
))
.limit(4);
return res.status(200).json({
numberOfPhotos: numberOfPhotos[0].count,
countriesInYear: countries,
countOfCities: cities.length,
albumsWithMostPhotos,
peopleWithMostPhotos: peopleWithMostPhotos,
favoritedAssets: favoritedAssets.map(asset => asset.id)
});
}
+123 -24
View File
@@ -1,33 +1,132 @@
import React from 'react'
import React, { useEffect, useMemo, useState } from 'react'
import { Player } from '@remotion/player';
import { IntroComposition } from '../../remotion/Composition';
import PageLayout from '@/components/layouts/PageLayout';
import Header from '@/components/shared/Header';
import { Button } from '@/components/ui/button';
import { getRewindStats } from '@/handlers/api/common.handler';
import { ASSET_PREVIEW_PATH, ASSET_THUMBNAIL_PATH, PERSON_THUBNAIL_PATH } from '@/config/routes';
const scenes = [
{ message: "Welcome to Immich Replay Varun!" },
{ message: "2024! So much memories!" },
{ message: "You've been to 10 new countries!" },
{ message: "And 25 new cities!" },
{ message: "Here are some of my favorite memories from this year!" },
{ message: "The most memory filled album of all time!" },
{ message: "You made so much memory with Soundarapandian"},
interface IStats {
numberOfPhotos: number;
countriesInYear: any[];
countOfCities: number;
albumsWithMostPhotos: any[];
peopleWithMostPhotos: any[];
favoritedAssets: string[];
}
const getScenes = (stats: IStats) => [
{
message: "Welcome Varun!",
type: "WELCOME",
emoji: "👋",
data: {},
},
{
message: "2024! So much memories!",
type: "TITLE",
emoji: "📅",
data: {
photos: stats.numberOfPhotos,
},
},
{
message: `You've been to ${stats.countriesInYear.length} new countries!`, type: "COUNTRY", emoji: "🌍", data: {
countries: stats.countriesInYear,
},
},
{
message: `And ${stats.countOfCities} new cities!`, type: "CITY", emoji: "🏙️", data: {
cities: stats.countOfCities,
},
},
{
message: "Here are some of my favorite memories from this year!", type: "MEMORY", emoji: "🎉", data: {
photos: stats.albumsWithMostPhotos,
images: stats.favoritedAssets.map((assetId) => ASSET_THUMBNAIL_PATH(assetId)),
},
},
{
message: "The most memory filled album of all time!", type: "ALBUM", emoji: "📸", data: {
photos: stats.albumsWithMostPhotos,
albums: stats.albumsWithMostPhotos.map((album) => ({
name: album.name,
cover: ASSET_THUMBNAIL_PATH(album.cover),
})),
},
},
{
message: "You made so much memory with", type: "FRIEND", emoji: "👯", data: {
friends: stats.peopleWithMostPhotos.map((person) => ({
name: person.name,
cover: PERSON_THUBNAIL_PATH(person.id),
})),
},
},
{
message: "Lets see what 2025 has in store!", type: "END", emoji: "🚀", data: {
},
},
]
export default function RewindPage() {
const [stats, setStats] = useState<any>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const fetchStats = async () => {
getRewindStats().then((stats) => {
setStats(stats);
setLoading(false);
}).catch((error) => {
setError(error);
setLoading(false);
});
}
const scenes = useMemo(() => {
if (!stats) return [];
return getScenes(stats);
}, [stats]);
useEffect(() => {
fetchStats();
}, []);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return (
<div>
<Player
component={IntroComposition}
inputProps={{ scenes: scenes }}
durationInFrames={scenes.length * 30}
compositionWidth={1280}
compositionHeight={720}
fps={30}
style={{
width: 1280,
height: 720,
}}
controls
/>
</div>
<PageLayout className="p-4">
<Header leftComponent="Rewind 2024" />
<div className="flex justify-center items-center w-full h-full">
<Player
component={IntroComposition}
inputProps={{ scenes: scenes }}
durationInFrames={scenes.length * 120}
compositionWidth={720}
compositionHeight={1280}
fps={30}
style={{
width: 720 / 2,
height: 1280 / 2,
}}
controls
/>
</div>
<div className="flex justify-center items-center w-full h-full">
<Button> Download Video</Button>
</div>
</PageLayout>
)
}