Skip to content

Commit

Permalink
Merge pull request #34 from biensupernice/feature/support-episode-tracks
Browse files Browse the repository at this point in the history
Support episode tracks
  • Loading branch information
jalvarado91 authored Mar 19, 2024
2 parents e3dd2f1 + 05e9b76 commit 1bdd367
Show file tree
Hide file tree
Showing 9 changed files with 1,130 additions and 418 deletions.
1,185 changes: 798 additions & 387 deletions package-lock.json

Large diffs are not rendered by default.

12 changes: 7 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,9 @@
"classnames": "^2.3.1",
"clsx": "^2.0.0",
"date-fns": "^2.27.0",
"eslint-config-next": "^13.4.8",
"framer-motion": "^7.5.3",
"lodash-es": "^4.17.21",
"mongodb": "^4.2.2",
"mongodb": "^6.5.0",
"next": "^13.4.8",
"nextjs-google-analytics": "^2.3.3",
"node-vibrant": "^3.2.1-alpha.1",
Expand All @@ -45,14 +44,17 @@
"zustand": "^3.6.7"
},
"devDependencies": {
"@types/eslint": "^8.44.7",
"@types/lodash": "^4.14.178",
"autoprefixer": "^10.4.16",
"eslint-config-next": "^13.4.8",
"lodash": "^4.17.21",
"eslint": "^8.54.0",
"postcss": "^8.4.31",
"prettier": "^2.7.1",
"prettier-plugin-tailwindcss": "^0.1.13",
"prettier": "^3.1.0",
"prettier-plugin-tailwindcss": "^0.5.7",
"tailwindcss": "^3.3.3",
"typescript": "4.9.5",
"typescript": "5.1.6",
"uuid": "^8.3.2"
}
}
95 changes: 92 additions & 3 deletions src/client/EpisodesScreen/EpisodeModalSheet/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
HeartFilled,
HeartOutline,
} from "@/client/components/Icons";
import { formatDate } from "@/client/helpers";
import { formatDate, formatTimeSecs } from "@/client/helpers";
import classNames from "classnames";
import { useFavorites, useIsFavoriteFast } from "../FavoritesStore";
import { MobilePlayerControls } from "../Player/MobilePlayerControls";
Expand All @@ -15,11 +15,14 @@ import {
usePlayerEpisodeDuration,
usePlayerLoadingStatus,
usePlayerActions,
usePlayerCuePosition,
} from "../PlayerStore";
import { useGetEpisode } from "../useEpisodeHooks";
import Sheet from "react-modal-sheet";
import create from "zustand";
import { useEffect } from "react";
import { trpc } from "@/utils/trpc";
import { cn } from "@/lib/utils";
import { EpisodeTrackProjection } from "@/server/router";

interface EpisodeModalSheetStore {
isOpen: boolean;
Expand Down Expand Up @@ -75,7 +78,7 @@ function EpisodeSheetContent({ episodeId }: { episodeId: string }) {

return (
<div className="relative flex h-full w-full flex-col items-center justify-between space-y-3 overflow-auto pb-safe-top">
<div className="w-full flex-col space-y-3 px-6 pt-6">
<div className="w-full flex-col space-y-3 px-4 md:px-6 pt-6">
<img
className="min-h-40 min-w-40 mx-auto w-full max-w-sm rounded-lg object-fill"
src={episode.artworkUrl}
Expand Down Expand Up @@ -106,10 +109,96 @@ function EpisodeSheetContent({ episodeId }: { episodeId: string }) {
</a>
<EpisodeSheetFavoriteToggle episodeId={episodeId} />
</div>
<EpisodeTracksList episodeId={episodeId} />
</div>
);
}

function EpisodeTracksList({ episodeId }: { episodeId: string }) {
const progress = usePlayerProgress();
const playerActions = usePlayerActions();
const progressSecs = progress / 1000;

const { data, status } = trpc["episode.getTracks"].useQuery({
episodeId,
});

const loaded = status === "success";
const loadedData = loaded ? (data ? data : []) : [];

const possibleTracks = loadedData.filter((t) =>
t.timestamp ? progressSecs >= t.timestamp : false
);

const currentTrack = possibleTracks.at(-1);

function onTrckClick(t: EpisodeTrackProjection) {
if (t.timestamp) {
playerActions.setCuePosition(t.timestamp * 1000);
}
}

return loaded && loadedData.length > 0 ? (
<div className="slide-in-from-bottom-3 animate-in duration-600">
<div className="py-1" />
<div className="py-4 rounded-lg text-white relative mx-4 bg-accent shadow border-accent">
<div className="absolute rounded-lg inset-0 bg-black/20"></div>
<div className="relative px-4 font-bold text-white text-lg mb-4">
{loadedData.length} Tracks
</div>
<div className="relative space-y">
{loadedData.map((t, i) => {
const isCurrent = currentTrack?.order === t.order;
return (
<button
onClick={() => onTrckClick(t)}
className="flex w-full justify-between items-center px-4 py-2 space-x-4"
>
<div className="flex text-left items-center space-x-3">
<div
className={cn(
"text-xs h-5 w-5 inline-flex p-1 items-center justify-center relative",
isCurrent && "bg-white text-accent rounded-full"
)}
>
{isCurrent && (
<div className="bg-white animate-ping [animation-duration:1500ms] absolute rounded-full origin-center p-2"></div>
)}
<div className="relative">{t.order}</div>
</div>
<div>
<div
className={cn(
"font-medium text-sm",
isCurrent && "!font-bold"
)}
>
{t.name}
</div>
<div
className={cn(
"text-white/80 text-sm",
isCurrent && "text-white/100"
)}
>
{t.artist}
</div>
</div>
</div>
{t.timestamp ? (
<div className="text-xs">{formatTimeSecs(t.timestamp)}</div>
) : (
<></>
)}
</button>
);
})}
</div>
</div>
</div>
) : null;
}

export interface EpisodeSheetFavoriteToggleProps {
episodeId: string;
}
Expand Down
2 changes: 1 addition & 1 deletion src/client/components/Episode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ export function Episode(props: EpisodeProps) {
);
}

function PlayingAnimation() {
export function PlayingAnimation() {
const isPlaying = usePlayerPlaying();

return (
Expand Down
60 changes: 60 additions & 0 deletions src/pages/api/internal/ingest-episode-tracks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { createDbConnection } from "@/server/db";
import {
trackInputSchema,
updateEpisodeDetailsBySoundcloudUrl,
} from "@/server/update-episode-details";
import { set } from "date-fns";
import { NextApiRequest, NextApiResponse } from "next";
import { z } from "zod";

const inputSchema = z.object({
soundCloudUrl: z.string(),
releaseDate: z.coerce
.date()
.transform((v) => set(v, { hours: 12 }))
.optional(),
tracks: z.array(trackInputSchema),
});

export default async (req: NextApiRequest, res: NextApiResponse) => {
try {
if (req.method !== "POST") {
return res.status(405).json({ error: "Method Not Allowed" });
}

const parsedInput = inputSchema.safeParse(req.body);
if (!parsedInput.success) {
const { errors } = parsedInput.error;

return res.status(400).json({
error: { message: "Invalid request", errors },
});
}

const db = await createDbConnection();
const updateRes = await updateEpisodeDetailsBySoundcloudUrl(
db,
parsedInput.data.soundCloudUrl,
{
releaseDate: parsedInput.data.releaseDate,
tracks: parsedInput.data.tracks,
}
);

if (!updateRes.ok) {
return res.status(400).json({
error: {
message: "Something went wrong ingesting episode tracks",
errors: updateRes.reason,
},
});
}

res.status(200).json({
message: "Data processed successfully",
});
} catch (err: any) {
console.error(err);
res.status(500).json({ msg: err.message });
}
};
6 changes: 3 additions & 3 deletions src/pages/api/internal/sync-episodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import _ from "lodash";
import { SoundCloudApiClient } from "@/server/crosscutting/soundCloudApiClient";
import { createDbConnection } from "@/server/db";
import { Db } from "mongodb";
import { DBTrack } from "@/server/router";
import { DBEpisode } from "@/server/router";

function createLargeSoundCloudThumbUrl(url: string) {
const newUrl = url.replace("-large", "-t500x500");
Expand Down Expand Up @@ -49,11 +49,11 @@ export async function getSoundCloudTracks(
url: track.permalink_url,
collective_slug: collectiveSlug,
picture_large: createLargeSoundCloudThumbUrl(track.artwork_url),
})) satisfies DBTrack[];
})) satisfies DBEpisode[];

let incomingIds = mapped.map((it) => it.key);

const trackCollection = db.collection<DBTrack>("tracksOld");
const trackCollection = db.collection<DBEpisode>("tracksOld");

let existing = await trackCollection
.find({
Expand Down
86 changes: 68 additions & 18 deletions src/server/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,31 +10,54 @@ import Vibrant from "node-vibrant";
import { initTRPC } from "@trpc/server";
import { syncAllCollectives } from "@/pages/api/internal/sync-episodes";

export type DBTrack = {
export type EpisodeTrack = {
order: number;
episode_id: ObjectId;
name: string;
artist: string;
timestamp?: number;
links: string[];
};

export type EpisodeTrackProjection = ReturnType<typeof episodeTrackProjection>;
export function episodeTrackProjection(t: EpisodeTrack) {
return {
order: t.order,
name: t.name,
artist: t.artist,
...(t.timestamp ? { timestamp: t.timestamp } : null),
} as const;
}

export type DBEpisode = {
source: "SOUNDCLOUD" | "MIXCLOUD";
duration: number;
created_time: Date;
release_date?: Date;
key: number | string;
name: string;
url: string;
picture_large: string;
collective_slug: "soulection" | "sasha-marie-radio" | "the-love-below-hour";
tracks?: EpisodeTrack[];
};

export type EpisodeProjection = ReturnType<typeof episodeProjection>;
export type EpisodeCollectiveSlugProjection = DBTrack["collective_slug"];
export function episodeProjection(t: WithId<DBTrack>) {
export type EpisodeCollectiveSlugProjection = DBEpisode["collective_slug"];
export function episodeProjection(e: WithId<DBEpisode>) {
return {
id: t._id.toString(),
source: t.source,
duration: t.duration,
releasedAt: t.created_time.toISOString(),
createadAt: t.created_time.toISOString(),
embedPlayerKey: t.key,
name: t.name,
permalinkUrl: t.url,
collectiveSlug: t.collective_slug,
artworkUrl: t.picture_large,
id: e._id.toString(),
source: e.source,
duration: e.duration,
releasedAt: e.release_date
? e.release_date.toISOString()
: e.created_time.toISOString(),
createadAt: e.created_time.toISOString(),
embedPlayerKey: e.key,
name: e.name,
permalinkUrl: e.url,
collectiveSlug: e.collective_slug,
artworkUrl: e.picture_large,
} as const;
}

Expand Down Expand Up @@ -74,7 +97,7 @@ export const episodeRouter = router({
};
}),
"internal.backfillCollectives": publicProcedure.query(({ ctx }) => {
const trackCollection = ctx.db.collection<DBTrack>("tracksOld");
const trackCollection = ctx.db.collection<DBEpisode>("tracksOld");

trackCollection.updateMany(
{},
Expand All @@ -98,9 +121,21 @@ export const episodeRouter = router({

let filter = collective === "all" ? {} : { collective_slug: collective };

const trackCollection = ctx.db.collection<DBTrack>("tracksOld");
const trackCollection = ctx.db.collection<DBEpisode>("tracksOld");
let allTracks = await trackCollection
.find(filter)
.find(filter, {
projection: {
source: 1,
duration: 1,
created_time: 1,
release_date: 1,
key: 1,
name: 1,
url: 1,
picture_large: 1,
collective_slug: 1,
},
})
.sort({
created_time: -1,
})
Expand All @@ -117,7 +152,7 @@ export const episodeRouter = router({
.query(async ({ input, ctx }) => {
const { episodeId } = input;

const trackCollection = ctx.db.collection<DBTrack>("tracksOld");
const trackCollection = ctx.db.collection<DBEpisode>("tracksOld");
const episode = await trackCollection.findOne({
_id: new ObjectId(episodeId),
});
Expand Down Expand Up @@ -152,7 +187,7 @@ export const episodeRouter = router({
return defaultAccentColor;
}

const trackCollection = ctx.db.collection<DBTrack>("tracksOld");
const trackCollection = ctx.db.collection<DBEpisode>("tracksOld");
const episode = await trackCollection.findOne({
_id: new ObjectId(episodeId),
});
Expand Down Expand Up @@ -190,6 +225,21 @@ export const episodeRouter = router({

return darkVibrantResult;
}),
"episode.getTracks": publicProcedure
.input(
z.object({
episodeId: z.string(),
})
)
.query(async ({ input, ctx }) => {
const trackCollection = ctx.db.collection<DBEpisode>("tracksOld");
const episode = await trackCollection.findOne({
_id: new ObjectId(input.episodeId),
});

const tracks = episode?.tracks ?? [];
return tracks.map(episodeTrackProjection);
}),
"episode.getFakeStreamUrl": publicProcedure
.input(
z.object({
Expand Down
Loading

1 comment on commit 1bdd367

@github-actions
Copy link

Choose a reason for hiding this comment

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

Deploy preview for soulector ready!

✅ Preview
https://soulector-qbsmimis6-jalvarado91.vercel.app

Built with commit 1bdd367.
This pull request is being automatically deployed with vercel-action

Please sign in to comment.