Skip to content

Commit

Permalink
adding fixSongMismatch function
Browse files Browse the repository at this point in the history
  • Loading branch information
DerLev committed Mar 27, 2024
1 parent 2d5d5ed commit 3a89125
Show file tree
Hide file tree
Showing 7 changed files with 289 additions and 1 deletion.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@ An application for creating playlists from the German radio station 1LIVE
- [ ] Add a webapp (most likely React/Next.js) to give users more insights on the playlists (show gathered data)
- [x] Add a job for filling up the backlog *(already done by hand)*
- [ ] Expand to other 1LIVE plalists (Neu für den Sektor, DIGGI) *(added NfdS)*
- [ ] Add function to replace tracks that have been wrongly mapped
- [x] Add function to replace tracks that have been wrongly mapped
3 changes: 3 additions & 0 deletions database/project/apiKey.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"invocationsKey": "Bearer auth token allowed to invoke HTTP functions"
}
4 changes: 4 additions & 0 deletions functions/src/firestoreDocumentTypes/ProjectCredentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,7 @@ export type PlaylistsDocument = {
weeklyTop100: string
newReleases: string
}

export type ApiKeyDocument = {
invocationsKey: string
}
50 changes: 50 additions & 0 deletions functions/src/firestoreDocumentTypes/SpotifyApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,3 +171,53 @@ export type UpdatePlaylistItems = {
export type RemovePlaylistItems = UpdatePlaylistItems

export type AddPlaylistItems = UpdatePlaylistItems

export type GetTrack = {
album: {
album_type: string
total_tracks: number
available_markets: string[]
external_urls: {
spotify: string
}
href: string
id: string
images: ImageObject[]
name: string
release_date: string
release_date_precision: "year" | "month" | "day"
restrictions?: {
reason: string
}
type: "album"
uri: string
artists: SimplifiedArtistObject[]
}
artists: ArtistObject[]
available_markets: string[]
disc_number: number
duration_ms: number
explicit: boolean
external_ids?: {
isrc?: string
ean?: string
upc?: string
}
external_urls: {
spotify: string
}
href: string
id: string
is_playable: boolean
linked_from?: object
restrictions?: {
reason: string
}
name: string
populartiy: number
preview_url: string | null
track_number: number
type: "track"
uri: string
is_local: boolean
}
39 changes: 39 additions & 0 deletions functions/src/helpers/validateAuthToken.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import {HttpsError, type Request} from "firebase-functions/v2/https";
import type {
ApiKeyDocument,
} from "../firestoreDocumentTypes/ProjectCredentials";
import type {Response} from "express";
import {db} from "./firebase";
import firestoreConverter from "./firestoreConverter";

const validateAuthToken = async (req: Request, res: Response) => {
const apiKeyDoc = (await db.collection("project").doc("apiKey")
.withConverter(firestoreConverter<ApiKeyDocument>()).get()).data();

if (apiKeyDoc === undefined) {
res.status(500).json({code: 500, message: "ApiKeyDoc does not exist"});
throw new HttpsError("internal", "ApiKeyDoc does not exist");
}

const reqApiKey = req.headers["authorization"]?.split(" ")[1];

if (!reqApiKey?.length) {
res.status(401).json({
code: 401,
message: "Add an API key to the Auth header",
});
throw new HttpsError("unauthenticated", "No API key in Auth header");
}

if (reqApiKey !== apiKeyDoc.invocationsKey) {
res.status(401).json({
code: 401,
message: "The supplied API key does not exist",
});
throw new HttpsError("unauthenticated", "Supplied API key does not exist");
}

return;
};

export default validateAuthToken;
191 changes: 191 additions & 0 deletions functions/src/playlistScraping/fixSongMismatch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
import {HttpsError, onRequest} from "firebase-functions/v2/https";
import onlyAllowMethods from "../helpers/onlyAllowMethods";
import validateAuthToken from "../helpers/validateAuthToken";
import {db} from "../helpers/firebase";
import firestoreConverter from "../helpers/firestoreConverter";
import type {SongsCollection} from "../firestoreDocumentTypes/SongsCollection";
import type {
TracksSubcollection,
} from "../firestoreDocumentTypes/PlaylistsCollection";
import type {
ArtistsCollection,
} from "../firestoreDocumentTypes/ArtistsCollection";
import {getClientToken} from "../helpers/spotifyTokens";
import type {
GetTrack,
SpotifyError,
} from "../firestoreDocumentTypes/SpotifyApi";
import {FieldValue, Timestamp} from "firebase-admin/firestore";
import generateSearchString from "../helpers/generateSearchString";

export const fixSongMismatch = onRequest(async (req, res) => {
onlyAllowMethods(req, res, ["PATCH"]);

await validateAuthToken(req, res);

const queryUid = req.query.uid?.toString();
const queryTrackUri = req.query.newTrackUri?.toString();

if (!queryUid?.length || !queryTrackUri?.length) {
res.status(400).json({
code: 400,
message: "uid and newTrackUri must be supplied",
});
throw new HttpsError(
"invalid-argument",
"uid and newTrackUri must be supplied"
);
}

const spotifyAccessToken = await getClientToken();

const songsCollection = db.collection("songs")
.withConverter(firestoreConverter<SongsCollection>());

const songDoc = await songsCollection.doc(queryUid).get();
const songDocData = songDoc.data();

if (songDocData === undefined) {
res.status(404).json({
code: 404,
message: "The SongDoc could not be found",
});
throw new HttpsError("not-found", "SongDoc not found");
}

const affectedTracks = (await db.collectionGroup("tracks")
.withConverter(firestoreConverter<TracksSubcollection>())
.where("trackUid", "==", queryUid).get()).docs;

const songNewUriQuery = await songsCollection
.where("spotifyTrackUri", "==", queryTrackUri).limit(1).get();

if (songNewUriQuery.empty) {
/* If the song was just badly mapped replace the doc */
const queryTrackId = queryTrackUri.split(":")[2];

/* Calling Spotify API */
const result = await fetch(
"https://api.spotify.com/v1/tracks/" + queryTrackId, {
method: "GET",
headers: {
"Authorization": "Bearer " + spotifyAccessToken,
},
}).then((res) => res.json()) as GetTrack | SpotifyError;

/* Handle errors returned by the Spotify Web API */
if ("error" in result) {
throw new Error(
"Spotify API returned an error: " + result.error.message
);
}

/* Get the unix epoch mills of the track release */
/* Spotify returns "2024", "2024-03", or "2024-03-26" */
const trackReleaseDateArr = result.album.release_date
.split("-");
const trackReleaseDateMills = Date.parse(trackReleaseDateArr[0] + "-" +
(trackReleaseDateArr[1] || "01") + "-" +
(trackReleaseDateArr[2] || "01"));

/* Isolate the artists and only get their names */
const artists = result.artists;
const artistNames = artists.map((item) => {
return item.name;
});

const artistsCollection = db.collection("artists")
.withConverter(firestoreConverter<ArtistsCollection>());

/* Get the artists from db -> return doc ref */
const artistRefsPromises = artists.map(async (item) => {
/* Get the artist's doc by Spotify URI */
const artistQuery = await artistsCollection
.where("spotifyUri", "==", item.uri).limit(1).get();

if (artistQuery.empty) {
/* Create artist if not in db */
const newDoc = await artistsCollection.add({
genres: [],
lastUpdated: FieldValue.serverTimestamp(),
name: item.name,
spotifyUri: item.uri,
searchString: generateSearchString(item.name),
});

const newDocRef = (await newDoc.get()).ref;
return newDocRef;
} else {
return artistQuery.docs[0].ref;
}
});

/* Execute artist search */
const artistRefs = await Promise.all(artistRefsPromises);

/* Add song to db */
await songDoc.ref.update({
title: result.name,
coverArt: result.album.images,
duration: result.duration_ms,
explicit: result.explicit,
released: Timestamp.fromMillis(trackReleaseDateMills),
releasePrecision: result.album.release_date_precision,
spotifyTrackUri: result.uri,
artists: artistRefs,
searchString: generateSearchString(
result.name,
artistNames.join(" & ")
),
});

const fixAffectedTracksPromsies = affectedTracks.map(async (track) => {
return await track.ref.update({
artists: artistNames,
duration: result.duration_ms,
explicit: result.explicit,
spotifyTrackUri: result.uri,
title: result.name,
});
});

await Promise.all(fixAffectedTracksPromsies);
} else {
/* If the song was badly mapped but exists in db */
const songFromDbDoc = songNewUriQuery.docs[0];
const songFromDbData = songFromDbDoc.data();

const songFromDbArtistsPromises = songFromDbData.artists.map(
async (artist) => {
return (await artist.get()).data();
}
);

const songFromDbArtists = (await Promise.all(songFromDbArtistsPromises))
.filter((item) => item !== undefined) as ArtistsCollection[];

const fixAffectedTracksPromsies = affectedTracks.map(async (track) => {
return await track.ref.update({
artists: songFromDbArtists.map((artist) => artist.name),
duration: songFromDbData.duration,
explicit: songFromDbData.explicit,
ref: songFromDbDoc.ref,
spotifyTrackUri: songFromDbData.spotifyTrackUri,
title: songFromDbData.title,
trackUid: songFromDbDoc.id,
});
});

await Promise.all(fixAffectedTracksPromsies);

await songFromDbDoc.ref.update({
searchString: songDocData.searchString,
});

if (songFromDbDoc.id !== songDoc.id) {
await songDoc.ref.delete();
}
}

res.json({code: 200, message: "Track URI replaced!"});
});
1 change: 1 addition & 0 deletions functions/src/playlistScraping/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from "./scrapeSchedule";
export * from "./scrapeNewReleasesProgramm";
export * from "./fixSongMismatch";

0 comments on commit 3a89125

Please sign in to comment.