Files
crates/api/src/discogs.ts
T

117 lines
3.8 KiB
TypeScript

import fs from 'fs/promises';
import axios from 'axios';
import cheerio from 'cheerio';
import Bluebird from 'bluebird';
import type Log from 'bunyan';
import type { PrismaClient, Want, User, Listing } from '@prisma/client';
import { logAxiosError, wantDetails } from './utils';
import { Pg } from './pg';
// margh
// vanderbabe
// jmcdermid1
export default function createDiscogs(log: Log, pg: Pg) {
const TOKEN = 'Discogs token=wRTXuYZGOPsIQgKRGKCAkUkidGtvpSfQgPWkYIpD';
const apiBase = 'https://api.discogs.com'
const discogsBase = 'https://www.discogs.com'
const api = axios.create({
headers: {
'Authorization': TOKEN,
// discogs only returns the first page without a desktop u/a
'User-Agent': 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0',
},
});
async function handleApiError(e) {
logAxiosError(e);
if (e.response.status === 429) {
throw new Error('rate limited');
}
return null;
}
async function wantListSync(username: string, user: User): Promise<Want[]> {
const { data: { wants, pagination }} = await api.get(`${apiBase}/users/${username}/wants`);
const releases = await Promise.all(wants.map(want => release(want.id)));
log.info({ wants: wants.length, user: username, userId: user.id }, 'fetched discogs wantlist');
// TODO filter releases to be vinyl format only
// convert want object to actual release and ignore duplicate errors
const results = await Promise.allSettled(releases.map(release => pg.wantCreate(release, user)));
// @ts-ignore
const additions = results.filter(r => r.status === 'fulfilled').map(r => r.value);
log.info('want list additions', additions);
return additions;
}
async function release(id: DiscogsRelease): Promise<any> {
const { data } = await api.get(`${apiBase}/releases/${id}`);
return data;
}
async function search(query: string) {
const url = new URL(`${apiBase}/database/search`);
url.searchParams.append('query', query);
url.searchParams.append('type', 'release');
url.searchParams.append('format', 'vinyl');
const { data: { results } } = await api.get(url.href);
return results;
}
async function inventory(user: string) {
const res = await api.get(`${apiBase}/users/${user}/inventory`);
return res.data.listings;
}
async function listings(want: Want) {
const res = await api.get(`https://www.discogs.com/sell/release/${want.release}?limit=250`);
const $ = cheerio.load(res.data);
const ids = $('.item_description_title[href]')
.map(function getListingId() {
const id = Number.parseInt(this.attribs.href.replace('/sell/item/', ''), 10);
return id;
})
.get();
const unlisted = await pg.listingRmUnlisted(want, ids);
if (unlisted) log.info({ unlisted: unlisted, ...wantDetails(want) }, 'missing listings removed');
// preferentially take listings from sellers if we hit req limits
return Bluebird.mapSeries(ids, async id => listing(id, want));
}
async function listing(id, want) {
const listing = await pg.listingFind(id);
if (listing) return listing;
try {
log.debug('fetching listing', id);
const res = await api.get(`${apiBase}/marketplace/listings/${id}`);
log.debug(res.data);
return pg.listingUpsert(res.data, want);
} catch (e) {
return handleApiError(e);
}
}
return {
wantListSync,
inventory,
listings,
listing,
release,
search,
};
}
export type Discogs = ReturnType<typeof createDiscogs>;