@@ -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%",
|
||||
|
||||
@@ -49,3 +49,6 @@ export const GET_FILTERS = BASE_API_ENDPOINT + "/filters/asset-filters";
|
||||
|
||||
// Person
|
||||
export const GET_PERSON_INFO = (personId: string) => BASE_API_ENDPOINT + "/people/" + personId + "/info";
|
||||
|
||||
// Rewind
|
||||
export const REWIND_STATS = BASE_API_ENDPOINT + "/rewind/stats";
|
||||
@@ -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";
|
||||
|
||||
@@ -10,3 +10,7 @@ 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);
|
||||
}
|
||||
@@ -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)
|
||||
});
|
||||
}
|
||||
+114
-15
@@ -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>
|
||||
<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 * 30}
|
||||
compositionWidth={1280}
|
||||
compositionHeight={720}
|
||||
durationInFrames={scenes.length * 120}
|
||||
compositionWidth={720}
|
||||
compositionHeight={1280}
|
||||
fps={30}
|
||||
style={{
|
||||
width: 1280,
|
||||
height: 720,
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user