diff --git a/frontend/src/App.js b/frontend/src/App.js
index 63af9ca..cf77910 100644
--- a/frontend/src/App.js
+++ b/frontend/src/App.js
@@ -1,22 +1,17 @@
import React from "react";
import { Route, Router, Switch } from "react-router-dom";
-import NavBar from "./components/NavBar";
-import PrivateRoute from "./components/PrivateRoute";
-import Profile from "./views/Profile";
-import ExternalApi from "./views/ExternalApi";
import history from "./utils/history";
+import { NavBar } from "./components/NavBar";
function App() {
return (
diff --git a/frontend/src/auth_config.json b/frontend/src/auth_config.json
deleted file mode 100644
index b8c8de1..0000000
--- a/frontend/src/auth_config.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "domain": "listory.eu.auth0.com",
- "clientId": "pYSTOYOsmenpCYj6TsRNQ8dprlqoQTt3",
- "audience": "https://listory.apricote.de/api/v1"
-}
diff --git a/frontend/src/components/NavBar.js b/frontend/src/components/NavBar.js
deleted file mode 100644
index b5bdd73..0000000
--- a/frontend/src/components/NavBar.js
+++ /dev/null
@@ -1,27 +0,0 @@
-import React from "react";
-import { useAuth0 } from "../react-auth0-spa";
-import { Link } from "react-router-dom";
-
-const NavBar = () => {
- const { isAuthenticated, loginWithRedirect, logout } = useAuth0();
-
- return (
-
- {!isAuthenticated && (
-
- )}
-
- {isAuthenticated && }
-
- {isAuthenticated && (
-
- Home
- Profile
- External API
-
- )}
-
- );
-};
-
-export default NavBar;
diff --git a/frontend/src/components/NavBar.jsx b/frontend/src/components/NavBar.jsx
new file mode 100644
index 0000000..e5aeb42
--- /dev/null
+++ b/frontend/src/components/NavBar.jsx
@@ -0,0 +1,9 @@
+import React from "react";
+
+export function NavBar() {
+ return (
+
+ );
+}
diff --git a/frontend/src/components/PrivateRoute.js b/frontend/src/components/PrivateRoute.js
deleted file mode 100644
index b1b1123..0000000
--- a/frontend/src/components/PrivateRoute.js
+++ /dev/null
@@ -1,26 +0,0 @@
-import React, { useEffect } from "react";
-import { Route } from "react-router-dom";
-import { useAuth0 } from "../react-auth0-spa";
-
-const PrivateRoute = ({ component: Component, path, ...rest }) => {
- const { loading, isAuthenticated, loginWithRedirect } = useAuth0();
-
- useEffect(() => {
- if (loading || isAuthenticated) {
- return;
- }
- const fn = async () => {
- await loginWithRedirect({
- appState: { targetUrl: path }
- });
- };
- fn();
- }, [loading, isAuthenticated, loginWithRedirect, path]);
-
- const render = props =>
- isAuthenticated === true ? : null;
-
- return ;
-};
-
-export default PrivateRoute;
diff --git a/frontend/src/index.js b/frontend/src/index.js
index cd7338d..8780a3a 100644
--- a/frontend/src/index.js
+++ b/frontend/src/index.js
@@ -2,31 +2,7 @@ import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import * as serviceWorker from "./serviceWorker";
-import { Auth0Provider } from "./react-auth0-spa";
-import config from "./auth_config.json";
-import history from "./utils/history";
-// A function that routes the user to the right place
-// after login
-const onRedirectCallback = appState => {
- history.push(
- appState && appState.targetUrl
- ? appState.targetUrl
- : window.location.pathname
- );
-};
-
-ReactDOM.render(
-
-
- ,
- document.getElementById("root")
-);
+ReactDOM.render(, document.getElementById("root"));
serviceWorker.unregister();
diff --git a/frontend/src/react-auth0-spa.js b/frontend/src/react-auth0-spa.js
deleted file mode 100644
index c004590..0000000
--- a/frontend/src/react-auth0-spa.js
+++ /dev/null
@@ -1,89 +0,0 @@
-import React, { useState, useEffect, useContext } from "react";
-import createAuth0Client from "@auth0/auth0-spa-js";
-
-const DEFAULT_REDIRECT_CALLBACK = () =>
- window.history.replaceState({}, document.title, window.location.pathname);
-
-export const Auth0Context = React.createContext();
-export const useAuth0 = () => useContext(Auth0Context);
-export const Auth0Provider = ({
- children,
- onRedirectCallback = DEFAULT_REDIRECT_CALLBACK,
- ...initOptions
-}) => {
- const [isAuthenticated, setIsAuthenticated] = useState();
- const [user, setUser] = useState();
- const [auth0Client, setAuth0] = useState();
- const [loading, setLoading] = useState(true);
- const [popupOpen, setPopupOpen] = useState(false);
-
- useEffect(() => {
- const initAuth0 = async () => {
- const auth0FromHook = await createAuth0Client(initOptions);
- setAuth0(auth0FromHook);
-
- if (
- window.location.search.includes("code=") &&
- window.location.search.includes("state=")
- ) {
- const { appState } = await auth0FromHook.handleRedirectCallback();
- onRedirectCallback(appState);
- }
-
- const isAuthenticated = await auth0FromHook.isAuthenticated();
-
- setIsAuthenticated(isAuthenticated);
-
- if (isAuthenticated) {
- const user = await auth0FromHook.getUser();
- setUser(user);
- }
-
- setLoading(false);
- };
- initAuth0();
- // eslint-disable-next-line
- }, []);
-
- const loginWithPopup = async (params = {}) => {
- setPopupOpen(true);
- try {
- await auth0Client.loginWithPopup(params);
- } catch (error) {
- console.error(error);
- } finally {
- setPopupOpen(false);
- }
- const user = await auth0Client.getUser();
- setUser(user);
- setIsAuthenticated(true);
- };
-
- const handleRedirectCallback = async () => {
- setLoading(true);
- await auth0Client.handleRedirectCallback();
- const user = await auth0Client.getUser();
- setLoading(false);
- setIsAuthenticated(true);
- setUser(user);
- };
- return (
- auth0Client.getIdTokenClaims(...p),
- loginWithRedirect: (...p) => auth0Client.loginWithRedirect(...p),
- getTokenSilently: (...p) => auth0Client.getTokenSilently(...p),
- getTokenWithPopup: (...p) => auth0Client.getTokenWithPopup(...p),
- logout: (...p) => auth0Client.logout(...p)
- }}
- >
- {children}
-
- );
-};
diff --git a/frontend/src/views/ExternalApi.js b/frontend/src/views/ExternalApi.js
deleted file mode 100644
index fd1ddf9..0000000
--- a/frontend/src/views/ExternalApi.js
+++ /dev/null
@@ -1,37 +0,0 @@
-import React, { useState } from "react";
-import { useAuth0 } from "../react-auth0-spa";
-
-const ExternalApi = () => {
- const [showResult, setShowResult] = useState(false);
- const [apiMessage, setApiMessage] = useState("");
- const { getTokenSilently } = useAuth0();
-
- const callApi = async () => {
- try {
- const token = await getTokenSilently();
-
- const response = await fetch("/api/v1/connections", {
- headers: {
- Authorization: `Bearer ${token}`
- }
- });
-
- const responseData = await response.json();
-
- setShowResult(true);
- setApiMessage(responseData);
- } catch (error) {
- console.error(error);
- }
- };
-
- return (
- <>
- External API
-
- {showResult && {JSON.stringify(apiMessage, null, 2)}}
- >
- );
-};
-
-export default ExternalApi;
diff --git a/frontend/src/views/Profile.js b/frontend/src/views/Profile.js
deleted file mode 100644
index 61be06a..0000000
--- a/frontend/src/views/Profile.js
+++ /dev/null
@@ -1,22 +0,0 @@
-import React, { Fragment } from "react";
-import { useAuth0 } from "../react-auth0-spa";
-
-const Profile = () => {
- const { loading, user } = useAuth0();
-
- if (loading || !user) {
- return Loading...
;
- }
-
- return (
-
-
-
- {user.name}
- {user.email}
- {JSON.stringify(user, null, 2)}
-
- );
-};
-
-export default Profile;
diff --git a/src/app.module.ts b/src/app.module.ts
index bb89b9a..ad223cc 100644
--- a/src/app.module.ts
+++ b/src/app.module.ts
@@ -1,17 +1,23 @@
import { Module } from "@nestjs/common";
import { ConfigModule } from "@nestjs/config";
+import { ScheduleModule } from "@nestjs/schedule";
import { AuthModule } from "./auth/auth.module";
import { DatabaseModule } from "./database/database.module";
+import { ListensModule } from "./listens/listens.module";
+import { MusicLibraryModule } from "./music-library/music-library.module";
import { SourcesModule } from "./sources/sources.module";
import { UsersModule } from "./users/users.module";
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }),
+ ScheduleModule.forRoot(),
DatabaseModule,
AuthModule,
UsersModule,
- SourcesModule
+ SourcesModule,
+ MusicLibraryModule,
+ ListensModule
]
})
export class AppModule {}
diff --git a/src/auth/spotify.strategy.ts b/src/auth/spotify.strategy.ts
index e31c9b0..20af2eb 100644
--- a/src/auth/spotify.strategy.ts
+++ b/src/auth/spotify.strategy.ts
@@ -7,8 +7,8 @@ import { AuthService } from "./auth.service";
@Injectable()
export class SpotifyStrategy extends PassportStrategy(Strategy) {
constructor(
- private readonly config: ConfigService,
- private readonly authService: AuthService
+ private readonly authService: AuthService,
+ config: ConfigService
) {
super({
clientID: config.get("SPOTIFY_CLIENT_ID"),
diff --git a/src/listens/dto/create-listen.dto.ts b/src/listens/dto/create-listen.dto.ts
new file mode 100644
index 0000000..ba54fb2
--- /dev/null
+++ b/src/listens/dto/create-listen.dto.ts
@@ -0,0 +1,8 @@
+import { Track } from "../../music-library/track.entity";
+import { User } from "../../users/user.entity";
+
+export class CreateListenDto {
+ track: Track;
+ user: User;
+ playedAt: Date;
+}
diff --git a/src/listens/listen.entity.ts b/src/listens/listen.entity.ts
new file mode 100644
index 0000000..c13535e
--- /dev/null
+++ b/src/listens/listen.entity.ts
@@ -0,0 +1,25 @@
+import {
+ Column,
+ Entity,
+ Index,
+ ManyToOne,
+ PrimaryGeneratedColumn
+} from "typeorm";
+import { Track } from "../music-library/track.entity";
+import { User } from "../users/user.entity";
+
+@Entity()
+@Index(["track", "user", "playedAt"], { unique: true })
+export class Listen {
+ @PrimaryGeneratedColumn("uuid")
+ id: string;
+
+ @ManyToOne(type => Track)
+ track: Track;
+
+ @ManyToOne(type => User)
+ user: User;
+
+ @Column({ type: "timestamp" })
+ playedAt: Date;
+}
diff --git a/src/listens/listen.repository.ts b/src/listens/listen.repository.ts
new file mode 100644
index 0000000..c0a099f
--- /dev/null
+++ b/src/listens/listen.repository.ts
@@ -0,0 +1,5 @@
+import { EntityRepository, Repository } from "typeorm";
+import { Listen } from "./listen.entity";
+
+@EntityRepository(Listen)
+export class ListenRepository extends Repository {}
diff --git a/src/listens/listens.module.ts b/src/listens/listens.module.ts
new file mode 100644
index 0000000..190e866
--- /dev/null
+++ b/src/listens/listens.module.ts
@@ -0,0 +1,11 @@
+import { Module } from "@nestjs/common";
+import { ListensService } from "./listens.service";
+import { TypeOrmModule } from "@nestjs/typeorm";
+import { ListenRepository } from "./listen.repository";
+
+@Module({
+ imports: [TypeOrmModule.forFeature([ListenRepository])],
+ providers: [ListensService],
+ exports: [ListensService]
+})
+export class ListensModule {}
diff --git a/src/listens/listens.service.spec.ts b/src/listens/listens.service.spec.ts
new file mode 100644
index 0000000..c800872
--- /dev/null
+++ b/src/listens/listens.service.spec.ts
@@ -0,0 +1,18 @@
+import { Test, TestingModule } from "@nestjs/testing";
+import { ListensService } from "./listens.service";
+
+describe("ListensService", () => {
+ let service: ListensService;
+
+ beforeEach(async () => {
+ const module: TestingModule = await Test.createTestingModule({
+ providers: [ListensService]
+ }).compile();
+
+ service = module.get(ListensService);
+ });
+
+ it("should be defined", () => {
+ expect(service).toBeDefined();
+ });
+});
diff --git a/src/listens/listens.service.ts b/src/listens/listens.service.ts
new file mode 100644
index 0000000..3100c84
--- /dev/null
+++ b/src/listens/listens.service.ts
@@ -0,0 +1,35 @@
+import { Injectable } from "@nestjs/common";
+import { Listen } from "./listen.entity";
+import { ListenRepository } from "./listen.repository";
+import { CreateListenDto } from "./dto/create-listen.dto";
+
+@Injectable()
+export class ListensService {
+ constructor(private readonly listenRepository: ListenRepository) {}
+
+ async createListen({
+ user,
+ track,
+ playedAt
+ }: CreateListenDto): Promise {
+ const listen = this.listenRepository.create();
+
+ listen.user = user;
+ listen.track = track;
+ listen.playedAt = playedAt;
+
+ try {
+ await this.listenRepository.save(listen);
+ } catch (err) {
+ if (err.code === "23505") {
+ return this.listenRepository.findOne({
+ where: { user, track, playedAt }
+ });
+ }
+
+ throw err;
+ }
+
+ return listen;
+ }
+}
diff --git a/src/music-library/album.entity.ts b/src/music-library/album.entity.ts
new file mode 100644
index 0000000..dbabf58
--- /dev/null
+++ b/src/music-library/album.entity.ts
@@ -0,0 +1,36 @@
+import {
+ Entity,
+ PrimaryGeneratedColumn,
+ Column,
+ ManyToMany,
+ JoinTable,
+ OneToMany
+} from "typeorm";
+import { SpotifyLibraryDetails } from "src/sources/spotify/spotify-library-details.entity";
+import { Artist } from "./artist.entity";
+import { Track } from "./track.entity";
+
+@Entity()
+export class Album {
+ @PrimaryGeneratedColumn("uuid")
+ id: string;
+
+ @Column()
+ name: string;
+
+ @ManyToMany(
+ type => Artist,
+ artist => artist.albums
+ )
+ @JoinTable()
+ artists: Artist[];
+
+ @OneToMany(
+ type => Track,
+ track => track.album
+ )
+ tracks: Track[];
+
+ @Column(type => SpotifyLibraryDetails)
+ spotify: SpotifyLibraryDetails;
+}
diff --git a/src/music-library/album.repository.ts b/src/music-library/album.repository.ts
new file mode 100644
index 0000000..805e9a9
--- /dev/null
+++ b/src/music-library/album.repository.ts
@@ -0,0 +1,5 @@
+import { EntityRepository, Repository } from "typeorm";
+import { Album } from "./album.entity";
+
+@EntityRepository(Album)
+export class AlbumRepository extends Repository {}
diff --git a/src/music-library/artist.entity.ts b/src/music-library/artist.entity.ts
new file mode 100644
index 0000000..1acd58f
--- /dev/null
+++ b/src/music-library/artist.entity.ts
@@ -0,0 +1,21 @@
+import { Entity, PrimaryGeneratedColumn, Column, ManyToMany } from "typeorm";
+import { SpotifyLibraryDetails } from "src/sources/spotify/spotify-library-details.entity";
+import { Album } from "./album.entity";
+
+@Entity()
+export class Artist {
+ @PrimaryGeneratedColumn("uuid")
+ id: string;
+
+ @Column()
+ name: string;
+
+ @ManyToMany(
+ type => Album,
+ album => album.artists
+ )
+ albums: Album[];
+
+ @Column(type => SpotifyLibraryDetails)
+ spotify: SpotifyLibraryDetails;
+}
diff --git a/src/music-library/artist.repository.ts b/src/music-library/artist.repository.ts
new file mode 100644
index 0000000..cbe90f0
--- /dev/null
+++ b/src/music-library/artist.repository.ts
@@ -0,0 +1,5 @@
+import { EntityRepository, Repository } from "typeorm";
+import { Artist } from "./artist.entity";
+
+@EntityRepository(Artist)
+export class ArtistRepository extends Repository {}
diff --git a/src/music-library/dto/create-album.dto.ts b/src/music-library/dto/create-album.dto.ts
new file mode 100644
index 0000000..3605198
--- /dev/null
+++ b/src/music-library/dto/create-album.dto.ts
@@ -0,0 +1,8 @@
+import { SpotifyLibraryDetails } from "../../sources/spotify/spotify-library-details.entity";
+import { Artist } from "../artist.entity";
+
+export class CreateAlbumDto {
+ name: string;
+ artists: Artist[];
+ spotify?: SpotifyLibraryDetails;
+}
diff --git a/src/music-library/dto/create-artist.dto.ts b/src/music-library/dto/create-artist.dto.ts
new file mode 100644
index 0000000..acaf4ae
--- /dev/null
+++ b/src/music-library/dto/create-artist.dto.ts
@@ -0,0 +1,6 @@
+import { SpotifyLibraryDetails } from "../../sources/spotify/spotify-library-details.entity";
+
+export class CreateArtistDto {
+ name: string;
+ spotify?: SpotifyLibraryDetails;
+}
diff --git a/src/music-library/dto/create-track.dto.ts b/src/music-library/dto/create-track.dto.ts
new file mode 100644
index 0000000..90a1158
--- /dev/null
+++ b/src/music-library/dto/create-track.dto.ts
@@ -0,0 +1,10 @@
+import { SpotifyLibraryDetails } from "../../sources/spotify/spotify-library-details.entity";
+import { Album } from "../album.entity";
+import { Artist } from "../artist.entity";
+
+export class CreateTrackDto {
+ album: Album;
+ artists: Artist[];
+ name: string;
+ spotify?: SpotifyLibraryDetails;
+}
diff --git a/src/music-library/dto/find-album.dto.ts b/src/music-library/dto/find-album.dto.ts
new file mode 100644
index 0000000..d3b2cfb
--- /dev/null
+++ b/src/music-library/dto/find-album.dto.ts
@@ -0,0 +1,5 @@
+export class FindAlbumDto {
+ spotify: {
+ id: string;
+ };
+}
diff --git a/src/music-library/dto/find-artist.dto.ts b/src/music-library/dto/find-artist.dto.ts
new file mode 100644
index 0000000..09db9f1
--- /dev/null
+++ b/src/music-library/dto/find-artist.dto.ts
@@ -0,0 +1,5 @@
+export class FindArtistDto {
+ spotify: {
+ id: string;
+ };
+}
diff --git a/src/music-library/dto/find-track.dto.ts b/src/music-library/dto/find-track.dto.ts
new file mode 100644
index 0000000..7ffddc5
--- /dev/null
+++ b/src/music-library/dto/find-track.dto.ts
@@ -0,0 +1,5 @@
+export class FindTrackDto {
+ spotify: {
+ id: string;
+ };
+}
diff --git a/src/music-library/music-library.module.ts b/src/music-library/music-library.module.ts
new file mode 100644
index 0000000..40cfc90
--- /dev/null
+++ b/src/music-library/music-library.module.ts
@@ -0,0 +1,19 @@
+import { Module } from "@nestjs/common";
+import { TypeOrmModule } from "@nestjs/typeorm";
+import { AlbumRepository } from "./album.repository";
+import { ArtistRepository } from "./artist.repository";
+import { MusicLibraryService } from "./music-library.service";
+import { TrackRepository } from "./track.repository";
+
+@Module({
+ imports: [
+ TypeOrmModule.forFeature([
+ AlbumRepository,
+ ArtistRepository,
+ TrackRepository
+ ])
+ ],
+ providers: [MusicLibraryService],
+ exports: [MusicLibraryService]
+})
+export class MusicLibraryModule {}
diff --git a/src/music-library/music-library.service.spec.ts b/src/music-library/music-library.service.spec.ts
new file mode 100644
index 0000000..7fe79c3
--- /dev/null
+++ b/src/music-library/music-library.service.spec.ts
@@ -0,0 +1,18 @@
+import { Test, TestingModule } from "@nestjs/testing";
+import { MusicLibraryService } from "./music-library.service";
+
+describe("MusicLibraryService", () => {
+ let service: MusicLibraryService;
+
+ beforeEach(async () => {
+ const module: TestingModule = await Test.createTestingModule({
+ providers: [MusicLibraryService]
+ }).compile();
+
+ service = module.get(MusicLibraryService);
+ });
+
+ it("should be defined", () => {
+ expect(service).toBeDefined();
+ });
+});
diff --git a/src/music-library/music-library.service.ts b/src/music-library/music-library.service.ts
new file mode 100644
index 0000000..580ca29
--- /dev/null
+++ b/src/music-library/music-library.service.ts
@@ -0,0 +1,78 @@
+import { Injectable } from "@nestjs/common";
+import { Album } from "./album.entity";
+import { AlbumRepository } from "./album.repository";
+import { Artist } from "./artist.entity";
+import { ArtistRepository } from "./artist.repository";
+import { CreateAlbumDto } from "./dto/create-album.dto";
+import { CreateArtistDto } from "./dto/create-artist.dto";
+import { CreateTrackDto } from "./dto/create-track.dto";
+import { FindAlbumDto } from "./dto/find-album.dto";
+import { FindArtistDto } from "./dto/find-artist.dto";
+import { FindTrackDto } from "./dto/find-track.dto";
+import { Track } from "./track.entity";
+import { TrackRepository } from "./track.repository";
+
+@Injectable()
+export class MusicLibraryService {
+ constructor(
+ private readonly albumRepository: AlbumRepository,
+ private readonly artistRepository: ArtistRepository,
+ private readonly trackRepository: TrackRepository
+ ) {}
+
+ async findArtist(query: FindArtistDto): Promise {
+ return this.artistRepository.findOne({
+ where: { spotify: { id: query.spotify.id } }
+ });
+ }
+
+ async createArtist(data: CreateArtistDto): Promise {
+ const artist = this.artistRepository.create();
+
+ artist.name = data.name;
+ artist.spotify = data.spotify;
+
+ console.log("createArtist", { data, artist });
+
+ await this.artistRepository.save(artist);
+
+ return artist;
+ }
+
+ async findAlbum(query: FindAlbumDto): Promise {
+ return this.albumRepository.findOne({
+ where: { spotify: { id: query.spotify.id } }
+ });
+ }
+
+ async createAlbum(data: CreateAlbumDto): Promise {
+ const album = this.albumRepository.create();
+
+ album.name = data.name;
+ album.artists = data.artists;
+ album.spotify = data.spotify;
+
+ await this.albumRepository.save(album);
+
+ return album;
+ }
+
+ async findTrack(query: FindTrackDto): Promise