This commit is contained in:
ntr committed 2022-08-18 00:34:46 +10:00
1 parent 30bf7426e4
commit 323c7877b1
13 files changed
+94 -52

No files matched your search

+4
View File
@@ -1,5 +1,6 @@
import createServices from './src/services'; import createServices from './src/services';
import createLog from './src/log'; import createLog from './src/log';
import { Events } from './src/events';
async function start() { async function start() {
const log = createLog(); const log = createLog();
@@ -14,8 +15,11 @@ async function start() {
pg, pg,
discogs, discogs,
api, api,
events,
} = createServices(log); } = createServices(log);
events.emit(Events.StartupSync);
await api.listen(); await api.listen();
} }
+3 -6
View File
@@ -153,12 +153,9 @@ export default function createApi(log: Log, pg: Pg, discogs, events) {
const { releaseId } = ctx.request.body; const { releaseId } = ctx.request.body;
const user = await pg.userFindId(ctx.state.user.id); const user = await pg.userFindId(ctx.state.user.id);
const release = await discogs.release(releaseId); const release = await discogs.release(releaseId);
const want = await pg.wantCreate([release], user); const want = await pg.wantCreate(release, user);
events.emit(Events.WantsAdded, [want]);
// fetch listings ctx.response.body = [want];
const listings = await discogs.listings(want);
ctx.response.body = { want, listings };
}); });
router.post('/want/discogs', async function wantListPost(ctx, next) { router.post('/want/discogs', async function wantListPost(ctx, next) {
+1 -1
View File
@@ -84,7 +84,7 @@ export default function discogs(log: Log, pg: Pg) {
try { try {
log.debug('fetching listing', id); log.debug('fetching listing', id);
const res = await api.get(`${apiBase}/marketplace/listings/${id}`); const res = await api.get(`${apiBase}/marketplace/listings/${id}`);
log.info(res.data); log.debug(res.data);
return pg.listingUpsert(res.data, want); return pg.listingUpsert(res.data, want);
} catch (e) { } catch (e) {
return handleApiError(e); return handleApiError(e);
+28 -3
View File
@@ -1,25 +1,50 @@
import { EventEmitter } from 'node:events'; import { EventEmitter } from 'node:events';
import type Log from 'bunyan'; import type Log from 'bunyan';
import type { Listing, User, Want, Prisma } from '@prisma/client'; import type { Want } from '@prisma/client';
import { Pg } from './pg'; import { Pg } from './pg';
import routerAuth from './routers/router.auth'; import routerAuth from './routers/router.auth';
export enum Events { export enum Events {
WantsAdded = 'WantsAdded', WantsAdded = 'WantsAdded',
StartupSync = 'StartupSync',
} }
export function eventService(log: Log, pg: Pg, discogs) { export function eventService(log: Log, pg: Pg, discogs) {
const events = new EventEmitter({ captureRejections: true }); const events = new EventEmitter({ captureRejections: true });
events.on('error', function eventError(e) { events.on('error', function eventError(e) {
log.error(e); log.error('event error', e);
}); });
events.on(Events.WantsAdded, async function wantsAdded(wants: Want[]) { events.on(Events.WantsAdded, async function wantsAdded(wants: Want[]) {
log.info('wants added', wants); for (const want of wants) {
const listings = await discogs.listings(want);
log.info({ count: listings.length, title: want.id }, 'listings added')
}
log.info('finished fetching listings', { wants: wants.length });
}); });
events.on(Events.StartupSync, async function wantsAdded() {
const wants = await pg.wantsToSync();
try {
for (const want of wants) {
const listings = await discogs.listings(want);
log.info({ count: listings.length, title: want.id }, 'listings added')
}
log.info('finished syncing', { wants: wants.length });
} catch (e) {
if (e.message === 'rate limited') {
log.warn('rate limited, trying again in 60s');
return setTimeout(() => events.emit(Events.StartupSync), 60 * 1000);
}
}
});
return events; return events;
} }
+6
View File
@@ -18,6 +18,7 @@ export interface Pg {
wantCreate: (discogsWant: any, user: User) => Promise<Want>, wantCreate: (discogsWant: any, user: User) => Promise<Want>,
wantList: (user: User) => Promise<Want[]>, wantList: (user: User) => Promise<Want[]>,
wantsToSync: () => Promise<Want[]>,
listingUpsert: (listing: Listing, want: Want) => Promise<Listing>, listingUpsert: (listing: Listing, want: Want) => Promise<Listing>,
listingFind: (id: DiscogsListing) => Promise<Listing>, listingFind: (id: DiscogsListing) => Promise<Listing>,
listingRmUnlisted: (want: Want, ids: DiscogsListing[]) => Promise<number>, listingRmUnlisted: (want: Want, ids: DiscogsListing[]) => Promise<number>,
@@ -60,6 +61,10 @@ export default function createPg(log: Log, prisma: PrismaClient): Pg {
}); });
} }
async function wantsToSync() {
return prisma.want.findMany({});
}
async function wantCreate(discogsRelease, user) { async function wantCreate(discogsRelease, user) {
const data: { userId: UUID, release: DiscogsListing, data: any } = { const data: { userId: UUID, release: DiscogsListing, data: any } = {
data: discogsRelease, data: discogsRelease,
@@ -149,6 +154,7 @@ export default function createPg(log: Log, prisma: PrismaClient): Pg {
wantCreate, wantCreate,
wantList, wantList,
wantsToSync,
listingUpsert, listingUpsert,
listingFind, listingFind,
listingRmUnlisted, listingRmUnlisted,
+13 -13
View File
@@ -1,5 +1,4 @@
import axios from 'axios'; import axios, { AxiosInstance } from 'axios';
import type { AxiosInstance } from 'axios';
type ApiResponse = [string | null, any | null]; type ApiResponse = [string | null, any | null];
@@ -20,20 +19,17 @@ const api = axios.create({
withCredentials: true withCredentials: true
}); });
// axios.interceptors.request.use(function onReq(config) { function onSuccess(response) {
// return config;
// }, function onError(error) {
// // console.error(error);
// Promise.resolve(['Network Error', null]);
// });
api.interceptors.response.use(function onSuccess(response) {
return [null, response.data]; return [null, response.data];
}, function onError(error) { }
// console.error(error);
function onError(error) {
if (error.response) return [error.response.data.error, null]; if (error.response) return [error.response.data.error, null];
if (error.request) return ['Network Error', null]; if (error.request) return ['Network Error', null];
}); return ['Unknown Error', null];
}
api.interceptors.response.use(onSuccess, onError);
const actions = { const actions = {
getBundles: async function getBundles() { getBundles: async function getBundles() {
@@ -52,6 +48,10 @@ const actions = {
return api.get('/want/all'); return api.get('/want/all');
}, },
postWantSearch: async function postWantSearch(artist, title) {
return api.post('/want/search', { artist, title });
},
postWant: async function postWant(releaseId) { postWant: async function postWant(releaseId) {
return api.post('/want', { releaseId }); return api.post('/want', { releaseId });
}, },
-1
View File
@@ -18,7 +18,6 @@ function App() {
useEffect(() => { useEffect(() => {
async function getLogin() { async function getLogin() {
const [error, user] = await actions.getLogin(); const [error, user] = await actions.getLogin();
console.log(error, user);
if (error) return route('/login'); if (error) return route('/login');
setUser(user); setUser(user);
route('/search'); route('/search');
+2 -3
View File
@@ -1,15 +1,14 @@
import { h } from 'preact'; import { h } from 'preact';
export default function Bundle(bundle) { export default function Bundle(bundle) {
console.log(bundle);
return ( return (
<div class="bundle"> <div class="bundle">
<h2>{bundle.username} - {bundle.seller}</h2> <h2>{bundle.username} - {bundle.seller}</h2>
<h3>{bundle.items.length} items - {bundle.total} {bundle.currency}</h3> <h3>{bundle.items.length} items - {bundle.total.toFixed(2)} {bundle.currency}</h3>
<h4>Ships: {bundle.mentionsAustralia && 'AU'} {bundle.mentionsWorldwide && 'WORLD'}</h4> <h4>Ships: {bundle.mentionsAustralia && 'AU'} {bundle.mentionsWorldwide && 'WORLD'}</h4>
{bundle.items.map(i => ( {bundle.items.map(i => (
<div key={i.id}> <div key={i.id}>
<a href={i.uri}> <a href={i.uri} target="_blank" >
<h4>{i.description}</h4> <h4>{i.description}</h4>
</a> </a>
<h5>{i.user.name}</h5> <h5>{i.user.name}</h5>
-1
View File
@@ -9,7 +9,6 @@ function Want(want) {
<div> <div>
<h3>{r.artists_sort} - {r.title}</h3> <h3>{r.artists_sort} - {r.title}</h3>
<h5 class='alt-text' >{r.year}</h5> <h5 class='alt-text' >{r.year}</h5>
<h5 class='alt-text'>{r.country}</h5>
<h5 class='alt-text'>{r.formats[0].descriptions.join(' ')}</h5> <h5 class='alt-text'>{r.formats[0].descriptions.join(' ')}</h5>
</div> </div>
<img src={r.thumb} /> <img src={r.thumb} />
-1
View File
@@ -11,7 +11,6 @@ function Home() {
async function getBundles() { async function getBundles() {
const [error, bundles] = await actions.getBundles(); const [error, bundles] = await actions.getBundles();
if (error) return console.error(error); if (error) return console.error(error);
console.log(bundles, 'bundles')
setBundles(bundles); setBundles(bundles);
} }
getBundles(); getBundles();
-1
View File
@@ -18,7 +18,6 @@ const List = ({ user }: Props) => {
async function getWants() { async function getWants() {
const [error, wants] = await actions.getWants(); const [error, wants] = await actions.getWants();
if (error) return console.error(error); if (error) return console.error(error);
console.log(wants, 'wants')
setOffers(wants); setOffers(wants);
} }
getWants(); getWants();
+29 -21
View File
@@ -1,20 +1,29 @@
import { h } from 'preact'; import { h } from 'preact';
import { useState } from 'preact/hooks'; import { useState } from 'preact/hooks';
import axios from 'axios'; import axios from 'axios';
import { ExternalLink } from 'react-feather'; import { ExternalLink, Check } from 'react-feather';
import actions from '../actions'; import actions from '../actions';
function result(r) { function Result(r) {
function onClick() { const [added, setAdded] = useState(false)
actions.postWant(r.id);
async function onClick() {
const [error, want] = await actions.postWant(r.id);
if (error) return false;
setAdded(true);
} }
const classes = `${added ? 'added' : ''}`;
const btnText = added
? <Check />
: '+';
return ( return (
<article key={r.id}> <article key={r.id}>
<button onClick={onClick}> + </button> <button onClick={onClick} disabled={added} class={classes} >{btnText}</button>
<div> <div>
<h3>{r.title} <a href={`https://discogs.com${r.uri}`}><ExternalLink /></a></h3> <h3>{r.title} <a href={`https://discogs.com${r.uri}`} target="_blank"><ExternalLink /></a></h3>
<h5 class='alt-text' >{r.year}</h5> <h5 class='alt-text' >{r.year}</h5>
<h5 class='alt-text'>{r.country}</h5> <h5 class='alt-text'>{r.country}</h5>
<h5 class='alt-text'>{r.format.join(' ')}</h5> <h5 class='alt-text'>{r.format.join(' ')}</h5>
@@ -25,33 +34,32 @@ function result(r) {
} }
const User = () => { const User = () => {
const [artist, setArtist] = useState('nine inch nails') const [artist, setArtist] = useState('')
const [title, setTitle] = useState('the fragile'); const [title, setTitle] = useState('');
const [username, setUsername] = useState(''); const [username, setUsername] = useState('');
const [results, setResults] = useState([]); const [results, setResults] = useState([]);
const [noResults, setNoResults] = useState(false); const [noResults, setNoResults] = useState(false);
async function fetchResults() { async function fetchResults() {
const { data } = await axios.post(`http://localhost:41337/want/search`, { const [error, results] = await actions.postWantSearch(artist, title);
artist,
title,
});
console.log(data); if (results.length === 0) {
if (data.length === 0) {
setResults([]); setResults([]);
return setNoResults(true); return setNoResults(true);
} }
setNoResults(false); setNoResults(false);
setResults(data); setResults(results);
} }
async function submitWantlist() { async function submitWantlist() {
const [error, wants] = await actions.postDiscogs(username); const [error, wants] = await actions.postDiscogs(username);
// setNoResults(false); if (results.length === 0) {
// setResults(data); setResults([]);
return setNoResults(true);
}
console.log('synced', wants.length, 'wants');
} }
function handleSubmit(e) { function handleSubmit(e) {
@@ -88,15 +96,15 @@ const User = () => {
<p>Sync Discogs Wantlist</p> <p>Sync Discogs Wantlist</p>
<form action="" method="get"> <form action="" method="get">
<label for="Discogs">Discogs Username</label> <label for="Discogs">Discogs Username</label>
<input type="text" name="username" placeholder="username" id="username" value={username} onChange={e => setUsername(e.target.value) } required /> <input type="text" name="username" placeholder="Discogs Username" id="username" value={username} onChange={e => setUsername(e.target.value) } required />
<input type="submit" value="Search" onClick={handleDiscogsSubmit} /> <input type="submit" value="Sync" onClick={handleDiscogsSubmit} />
</form> </form>
</div> </div>
</div> </div>
<div /> <div />
<div> <div>
{noResultsEl} {noResultsEl}
{results.map(result)} {results.map(Result)}
{topEl} {topEl}
</div> </div>
</div> </div>
+8 -1
View File
@@ -56,6 +56,7 @@ html {
--color-accent-blue: #5E9ED6; --color-accent-blue: #5E9ED6;
--color-blue-gray: #506485; --color-blue-gray: #506485;
--color-green: #537a34;
--base-accent-color: var(--color-accent-blue); --base-accent-color: var(--color-accent-blue);
--fg-color: var(--color-off-white); --fg-color: var(--color-off-white);
@@ -200,14 +201,20 @@ article {
margin-right: var(--base-spacing); margin-right: var(--base-spacing);
background: var(--tertiary-background-color); background: var(--tertiary-background-color);
border-color: var(--secondary-background-color); border-color: var(--secondary-background-color);
&.added {
background: var(--color-green);
cursor: auto;
}
} }
} }
.list { .list {
display: flex; display: flex;
flex-flow: row wrap;
article { article {
flex: 1; flex: 1 1 20%;
padding: var(--base-spacing); padding: var(--base-spacing);
margin: var(--base-spacing-half); margin: var(--base-spacing-half);
} }