Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions .babelrc
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
{
"presets": [
"next/babel"
],
"presets": ["next/babel"],
"plugins": [
["styled-components", {
"displayName": false,
"ssr": true
}]
[
"styled-components",
{
"displayName": false,
"ssr": true
}
]
]
}
2 changes: 2 additions & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/usr/bin/env sh
yarn lint-staged
11 changes: 11 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
.next
node_modules
coverage
dist
build
out
test-results
public/static
.yarn
.pnp.*
yarn.lock
7 changes: 7 additions & 0 deletions .prettierrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"semi": true,
"singleQuote": false,
"trailingComma": "all",
"arrowParens": "avoid",
"printWidth": 120
}
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ In addition to the software requirements, you need access to the following APIs:
- [Discogs](https://www.discogs.com/applications/edit) for searching and getting release info
- [Last.fm](https://www.last.fm/api/account/create) for authentication and scrobbling data


### Installing

First you need to clone the repository from github:
Expand Down Expand Up @@ -85,6 +84,7 @@ yarn test
```

You can also use watch-mode and display the current test coverage:

```
yarn test:watch
yarm test:coverage
Expand All @@ -110,7 +110,7 @@ yarn start

## Authors

* **Daniel Puscher** - *Initial work* - [dpuscher](https://github.com/dpuscher)
- **Daniel Puscher** - _Initial work_ - [dpuscher](https://github.com/dpuscher)

See also the list of [contributors](https://github.com/dpuscher/code-scrobble/contributors) who participated in this project.

Expand Down
4 changes: 2 additions & 2 deletions app/__mocks__/redis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@

const store = new Map();

const mockGet = jest.fn(async (key) => store.get(key) || null);
const mockGet = jest.fn(async key => store.get(key) || null);
const mockSet = jest.fn(async (key, value) => {
store.set(key, value);
return 'OK';
return "OK";
});
const mockConnect = jest.fn(async () => {});
const mockOn = jest.fn();
Expand Down
7 changes: 3 additions & 4 deletions app/cache.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { createClient } from 'redis';
import { createClient } from "redis";

const client = createClient({ url: process.env.REDISCLOUD_URL });
client.connect().catch(console.error);
client.on('error', console.error);
client.on("error", console.error);

export const set = (key: string, value: unknown, ttl = 86400) =>
client.set(key, JSON.stringify(value), { EX: ttl });
export const set = (key: string, value: unknown, ttl = 86400) => client.set(key, JSON.stringify(value), { EX: ttl });

export const get = async <T = unknown>(key: string): Promise<T> => {
const value = await client.get(key);
Expand Down
79 changes: 40 additions & 39 deletions app/discogs.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import orderBy from 'lodash/orderBy';
import pick from 'lodash/pick';
import find from 'lodash/find';
import * as Cache from './cache';
import orderBy from "lodash/orderBy";
import pick from "lodash/pick";
import find from "lodash/find";
import * as Cache from "./cache";

// eslint-disable-next-line @typescript-eslint/no-require-imports
const DiscogsClient = require('disconnect').Client;
const DiscogsClient = require("disconnect").Client;

interface DiscogsError {
statusCode: number;
Expand Down Expand Up @@ -71,19 +71,17 @@ const Database = new DiscogsClient({
const convertTimecode = (timecode: string | undefined): number => {
if (!timecode) return 0;
return timecode
.split(':')
.map(n => (parseInt(n, 10) || 0))
.split(":")
.map(n => parseInt(n, 10) || 0)
.reverse()
.map((n, i) => n * (60 ** i))
.map((n, i) => n * 60 ** i)
.reduce((pv, cv) => pv + cv);
};

const normalizeTracklist = (tracks: DiscogsTrack[]): DiscogsTrack[] => {
const vinylPositionRegex = /^[A-Z]-?[0-9]+$/;
// eslint-disable-next-line no-underscore-dangle
let tracklist = tracks
.filter(track => track.type_ === 'track')
.filter(track => !/video/i.test(track.position));
let tracklist = tracks.filter(track => track.type_ === "track").filter(track => !/video/i.test(track.position));

// Remove Bonus CDs from vinyl releases:
if (tracklist.length && vinylPositionRegex.test(tracklist[0].position)) {
Expand All @@ -97,67 +95,70 @@ const normalizeTracklist = (tracks: DiscogsTrack[]): DiscogsTrack[] => {
};

const getBarcode = (data: DiscogsIdentifier[] = []): string | undefined =>
(find(data, { type: 'Barcode' }) || {}).value;
(find(data, { type: "Barcode" }) || {}).value;

const buildRelease = (id: number, data: DiscogsData): ReleaseData => ({
id,
artist: (data.artists || []).map(a => a.name).join(', '),
artist: (data.artists || []).map(a => a.name).join(", "),
title: data.title,
image: data?.images?.[0]?.uri,
url: data.uri,
year: data.year,
tracks: normalizeTracklist(data.tracklist || [])
.map((track, index) => ({
title: track.title,
trackNumber: index + 1,
duration: convertTimecode(track.duration),
})),
tracks: normalizeTracklist(data.tracklist || []).map((track, index) => ({
title: track.title,
trackNumber: index + 1,
duration: convertTimecode(track.duration),
})),
barcode: getBarcode(data.identifiers),
});

export const barcode = (barcodeValue: string): Promise<number | undefined> =>
new Promise((resolve) => {
new Promise(resolve => {
const cacheKey = `barcode--${barcodeValue}`;

Cache.get<number>(cacheKey)
.then((result) => {
.then(result => {
resolve(result);
})
.catch(() => {
Database.search(undefined, { barcode: barcodeValue, type: 'release' }, (err: DiscogsError, data: { results: DiscogsSearchResult[] }) => {
if (err || !data || !data.results || !data.results.length) {
return resolve(undefined);
}
const results = orderBy(
data.results,
['community.have', 'community.want'],
['desc', 'desc'],
) as DiscogsSearchResult[];
Database.search(
undefined,
{ barcode: barcodeValue, type: "release" },
(err: DiscogsError, data: { results: DiscogsSearchResult[] }) => {
if (err || !data || !data.results || !data.results.length) {
return resolve(undefined);
}
const results = orderBy(
data.results,
["community.have", "community.want"],
["desc", "desc"],
) as DiscogsSearchResult[];

Cache.set(cacheKey, results[0].id);
Cache.set(cacheKey, results[0].id);

return resolve(results[0].id);
});
return resolve(results[0].id);
},
);
});
});

export const search = (query: string): Promise<SearchResult[] | undefined> =>
new Promise((resolve) => {
new Promise(resolve => {
const cacheKey = `search--${query}`;

Cache.get<SearchResult[]>(cacheKey)
.then((results) => {
.then(results => {
resolve(results);
})
.catch(() => {
// eslint-disable-next-line consistent-return
Database.search(query, { type: 'release' }, (err: DiscogsError, data: { results: any[] }) => {
Database.search(query, { type: "release" }, (err: DiscogsError, data: { results: any[] }) => {
if (err || !data || !data.results || !data.results.length) {
return resolve(undefined);
}

const results: SearchResult[] = data.results.map(result =>
pick(result, ['id', 'title', 'thumb', 'country', 'year', 'format', 'uri']),
pick(result, ["id", "title", "thumb", "country", "year", "format", "uri"]),
);

Cache.set(cacheKey, results);
Expand All @@ -174,7 +175,7 @@ export const getRelease = (id: number): Promise<ReleaseData> =>
if (err.statusCode === 404) {
Database.getMaster(id, (masterErr: DiscogsError, masterData: DiscogsData) => {
if (masterErr || !masterData) {
reject(masterErr || new Error('No data returned from Discogs'));
reject(masterErr || new Error("No data returned from Discogs"));
} else {
resolve(buildRelease(id, masterData));
}
Expand All @@ -183,7 +184,7 @@ export const getRelease = (id: number): Promise<ReleaseData> =>
reject(err);
}
} else if (!data) {
reject(new Error('No data returned from Discogs'));
reject(new Error("No data returned from Discogs"));
} else {
resolve(buildRelease(id, data));
}
Expand Down
28 changes: 11 additions & 17 deletions app/lastfm.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// eslint-disable-next-line @typescript-eslint/no-require-imports
const LastFMApi = require('lastfmapi');
const LastFMApi = require("lastfmapi");

interface ScrobbleTrack {
album: string;
Expand Down Expand Up @@ -28,7 +28,7 @@ const getScrobble = (data: ReleaseForScrobble): ScrobbleTrack[] => {
let nextTimestamp = Math.floor(Date.now() / 1000);
const trackData: ScrobbleTrack[] = [];

data.tracks.forEach((track) => {
data.tracks.forEach(track => {
trackData.push({
album: data.title,
artist: data.artist,
Expand All @@ -44,27 +44,21 @@ const getScrobble = (data: ReleaseForScrobble): ScrobbleTrack[] => {

export const scrobbleTracks = (username: string, key: string, data: ReleaseForScrobble): Promise<unknown> =>
new Promise((resolve, reject) => {
createApiClient(username, key).track.scrobble(
getScrobble(data),
(err: unknown, scrobbles: unknown) => {
if (err) reject(err);
resolve(scrobbles);
},
);
createApiClient(username, key).track.scrobble(getScrobble(data), (err: unknown, scrobbles: unknown) => {
if (err) reject(err);
resolve(scrobbles);
});
});

export interface LastFMUserData {
url: string;
image?: Array<{ '#text': string }>;
image?: Array<{ "#text": string }>;
}

export const getUserData = (username: string, key: string): Promise<LastFMUserData> =>
new Promise((resolve, reject) => {
createApiClient(username, key).user.getInfo(
null,
(err: unknown, userData: unknown) => {
if (err) reject(err);
resolve(userData as LastFMUserData);
},
);
createApiClient(username, key).user.getInfo(null, (err: unknown, userData: unknown) => {
if (err) reject(err);
resolve(userData as LastFMUserData);
});
});
36 changes: 21 additions & 15 deletions app/models/release.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import mongoose, { Schema, HydratedDocument, Model } from 'mongoose';
import pick from 'lodash/pick';
import * as Discogs from '../discogs';
import mongoose, { Schema, HydratedDocument, Model } from "mongoose";
import pick from "lodash/pick";
import * as Discogs from "../discogs";

interface ITrack {
title: string;
Expand All @@ -27,7 +27,10 @@ interface IReleaseMethods {

interface IReleaseModel extends Model<IRelease, object, IReleaseMethods> {
createFromDiscogs(id: number, barcode?: string): Promise<HydratedDocument<IRelease, IReleaseMethods>>;
firstOrCreate(param: { id?: string | number; barcode?: string }): Promise<HydratedDocument<IRelease, IReleaseMethods> | null>;
firstOrCreate(param: {
id?: string | number;
barcode?: string;
}): Promise<HydratedDocument<IRelease, IReleaseMethods> | null>;
}

const releaseSchema = new Schema<IRelease, IReleaseModel, IReleaseMethods>(
Expand All @@ -41,11 +44,13 @@ const releaseSchema = new Schema<IRelease, IReleaseModel, IReleaseMethods>(
image: String,
url: String,
year: String,
tracks: [{
title: String,
trackNumber: Number,
duration: Number,
}],
tracks: [
{
title: String,
trackNumber: Number,
duration: Number,
},
],
barcode: String,
},
{ timestamps: true },
Expand All @@ -60,7 +65,7 @@ releaseSchema.methods.toJSON = function toJSON() {
image: this.image,
url: this.url,
year: this.year,
tracks: this.tracks.map((track: ITrack) => pick(track, ['title', 'trackNumber', 'duration'])),
tracks: this.tracks.map((track: ITrack) => pick(track, ["title", "trackNumber", "duration"])),
};
};

Expand All @@ -82,7 +87,7 @@ releaseSchema.methods.updateFromDiscogs = async function updateFromDiscogs() {
};

releaseSchema.statics.createFromDiscogs = async function createFromDiscogs(id: number, barcodeValue?: string) {
const release = new (this)({ id });
const release = new this({ id });
if (barcodeValue) release.barcode = barcodeValue;
await release.updateFromDiscogs();
return release;
Expand All @@ -92,12 +97,13 @@ releaseSchema.statics.firstOrCreate = async function firstOrCreate(param: { id?:
const release = await this.findOne(param).exec();
if (!release) {
const paramId = param.id ? Number(param.id) : undefined;
const id = paramId || await Discogs.barcode(param.barcode);
const id = paramId || (await Discogs.barcode(param.barcode));
if (!id) return null;

try {
return await this.createFromDiscogs(id, param.barcode);
} catch (err: any) { // eslint-disable-line @typescript-eslint/no-explicit-any
} catch (err: any) {
// eslint-disable-line @typescript-eslint/no-explicit-any
if (err.code === 11000) {
return this.findOne({ id }).exec();
}
Expand All @@ -113,7 +119,7 @@ releaseSchema.statics.firstOrCreate = async function firstOrCreate(param: { id?:
return release;
};

const Release = (mongoose.models.Release as IReleaseModel) ||
mongoose.model<IRelease, IReleaseModel>('Release', releaseSchema);
const Release =
(mongoose.models.Release as IReleaseModel) || mongoose.model<IRelease, IReleaseModel>("Release", releaseSchema);

export default Release;
Loading