Skip to content

Commit

Permalink
Added observations & fixed clip timing.
Browse files Browse the repository at this point in the history
  • Loading branch information
changesbyjames committed Nov 11, 2024
1 parent a467c12 commit a00a075
Show file tree
Hide file tree
Showing 40 changed files with 860 additions and 212 deletions.
1 change: 1 addition & 0 deletions census/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"npm": "^10.8.3",
"oslo": "^1.2.1",
"postgres": "^3.4.4",
"sharp": "^0.33.5",
"tsx": "^4.19.0",
"yauzl": "^3.1.3",
"zod": "^3.23.8"
Expand Down
29 changes: 18 additions & 11 deletions census/api/src/api/capture.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import { z } from 'zod';
import { subscribeToChanges } from '../db/listen.js';
import { completeCaptureRequest, createFromClip, getCapture } from '../services/capture/index.js';
import {
completeCaptureRequest,
createFromClip,
getCapture,
getCaptureCount,
getCaptures
} from '../services/capture/index.js';
import { downloadClip } from '../services/twitch/clips.js';
import { procedure, router } from '../trpc/trpc.js';
import { Pagination } from './observation.js';

export default router({
capture: procedure.input(z.object({ id: z.number() })).query(async ({ input }) => {
Expand All @@ -21,6 +28,15 @@ export default router({
})
},

captures: procedure.input(z.object({ meta: Pagination })).query(async ({ input }) => {
const data = await getCaptures(input.meta);
const count = await getCaptureCount();
return {
meta: { ...input.meta, total: count },
data
};
}),

createFromClip: procedure
.input(z.object({ id: z.string(), userIsVerySureItIsNeeded: z.boolean().optional() }))
.mutation(async ({ input }) => {
Expand All @@ -32,14 +48,5 @@ export default router({
});
}
return clip;
}),

addPoints: procedure.input(z.object({ points: z.number() })).mutation(async ({ input, ctx }) => {
points += input.points;
ctx.points(points);
await new Promise(resolve => setTimeout(resolve, 300));
return { hello: 'world' };
})
})
});

let points = 0;
19 changes: 15 additions & 4 deletions census/api/src/api/identification.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,29 @@
import { z } from 'zod';
import { suggestIdentification } from '../services/identifications/identifications.js';
import { getTaxaFromPartialSearch } from '../services/inat/index.js';
import { recordAchievement } from '../services/points/achievement.js';
import { procedure, router } from '../trpc/trpc';
import { useUser } from '../utils/env/env.js';

export default router({
vote: procedure
.input(
z.object({
id: z.string(),
id: z.number(),
vote: z.enum(['up', 'down']),
comment: z.string().optional()
})
)
.mutation(async ({ input, ctx }) => {
const points = await recordAchievement('vote', ctx.user);
ctx.points(points);
})
const user = useUser();
const points = await recordAchievement('vote', user.id);
if (points) ctx.points(points);
}),
searchForTaxa: procedure.input(z.object({ query: z.string() })).query(async ({ input }) => {
return await getTaxaFromPartialSearch(input.query);
}),

suggest: procedure.input(z.object({ observationId: z.number(), iNatId: z.number() })).mutation(async ({ input }) => {
return await suggestIdentification(input.observationId, input.iNatId);
})
});
2 changes: 2 additions & 0 deletions census/api/src/api/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { router } from '../trpc/trpc.js';
import capture from './capture.js';
import feed from './feed.js';
import identification from './identification.js';
import me from './me.js';
import observation from './observation.js';
import twitch from './twitch.js';
Expand All @@ -9,5 +10,6 @@ export default router({
feed,
capture,
observation,
identification,
twitch
});
37 changes: 34 additions & 3 deletions census/api/src/api/observation.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,42 @@
import { z } from 'zod';
import { createObservationsFromCapture, ObservationPayload } from '../services/observations/observations.js';
import { editorProcedure, router } from '../trpc/trpc.js';
import {
createObservationsFromCapture,
getObservationCount,
getObservations,
ObservationPayload
} from '../services/observations/observations.js';
import { editorProcedure, procedure, router } from '../trpc/trpc.js';

export const Pagination = z.object({
page: z.number().default(1),
size: z.number().default(30)
});

export type Pagination = z.infer<typeof Pagination>;

export const Query = z.object({
start: z.coerce.date().optional(),
end: z.coerce.date().optional()
});

export type Query = z.infer<typeof Query>;

export default router({
createObservationsFromCapture: editorProcedure
.input(z.object({ captureId: z.number(), observations: z.array(ObservationPayload) }))
.mutation(async ({ input }) => {
return await createObservationsFromCapture(input.captureId, input.observations);
})
}),

list: procedure.input(z.object({ meta: Pagination, query: Query.optional() })).query(async ({ input }) => {
const count = await getObservationCount();
const data = await getObservations(input.meta);
return {
meta: {
...input.meta,
total: count
},
data
};
})
});
1 change: 1 addition & 0 deletions census/api/src/db/schema/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ export const observationsRelations = relations(observations, ({ one, many }) =>
fields: [observations.captureId],
references: [captures.id]
}),
identifications: many(identifications),
images: many(images)
}));

Expand Down
5 changes: 5 additions & 0 deletions census/api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { createContext } from './trpc/context.js';
// NOT the router itself.
export type AppRouter = typeof router;

import { getEncodedTimestamp } from './services/twitch/index.js';
import { createEnvironment, withEnvironment } from './utils/env/env.js';

(async () => {
Expand All @@ -32,6 +33,10 @@ import { createEnvironment, withEnvironment } from './utils/env/env.js';
} satisfies FastifyTRPCPluginOptions<AppRouter>['trpcOptions']
});
server.listen({ port: Number(process.env.PORT), host: process.env.HOST }, async (err, address) => {
const pixels = await getEncodedTimestamp(
'https://static-cdn.jtvnw.net/twitch-clips-thumbnails-prod/OilyClumsyLorisLitFam-UhVlgBoVRPDlk1X1/0339a75a-4829-4242-8e36-dcd08f2f4793/preview.jpg'
);
console.log(pixels);
if (err) {
console.error(err);
process.exit(1);
Expand Down
18 changes: 17 additions & 1 deletion census/api/src/services/capture/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { and, eq, gte, lte, or } from 'drizzle-orm';
import { and, count, desc, eq, gte, lte, or } from 'drizzle-orm';
import { Pagination } from '../../api/observation.js';
import { Capture, captures } from '../../db/schema/index.js';
import { useDB } from '../../db/transaction.js';
import { useUser } from '../../utils/env/env.js';
Expand Down Expand Up @@ -152,6 +153,21 @@ export const getCapture = async (id: number) => {
return capture;
};

export const getCaptureCount = async () => {
const db = useDB();
const [result] = await db.select({ count: count() }).from(captures);
return result.count;
};

export const getCaptures = async (pagination: Pagination) => {
const db = useDB();
return await db.query.captures.findMany({
limit: pagination.size,
offset: (pagination.page - 1) * pagination.size,
orderBy: [desc(captures.capturedAt)]
});
};

export const getCaptureByClipId = async (id: string) => {
const db = useDB();
return await db.query.captures.findFirst({
Expand Down
27 changes: 27 additions & 0 deletions census/api/src/services/identifications/identifications.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { identifications } from '../../db/schema/index.js';
import { useDB } from '../../db/transaction.js';
import { useUser } from '../../utils/env/env.js';
import { getTaxaInfo } from '../inat/index.js';

export const suggestIdentification = async (observationId: number, iNatId: number) => {
const source = await getTaxaInfo(iNatId);
return createIdentification(observationId, iNatId, source.preferred_common_name ?? source.name);
};

export const createIdentification = async (observationId: number, iNatId: number, name: string) => {
const db = useDB();
const user = useUser();

const [identification] = await db
.insert(identifications)
.values({
name,
nickname: name,
sourceId: iNatId.toString(),
observationId,
suggestedBy: user.id
})
.returning();

return identification;
};
7 changes: 7 additions & 0 deletions census/api/src/services/inat/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,10 @@ export const getTaxaFromPartialSearch = async (search: string) => {
const data = await response.json();
return SearchResults.parse(data);
};

export const getTaxaInfo = async (iNatId: number) => {
const response = await fetch(`https://api.inaturalist.org/v1/taxa/${iNatId}`);
const data = await response.json();
const { results } = SearchResults.parse(data);
return results[0];
};
25 changes: 21 additions & 4 deletions census/api/src/services/observations/observations.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { eq } from 'drizzle-orm';
import { count, desc, eq } from 'drizzle-orm';
import { ReadableStream } from 'node:stream/web';
import { Readable } from 'stream';
import { BoundingBox, images, observations } from '../../db/schema';
Expand Down Expand Up @@ -79,6 +79,25 @@ const createObservations = async (captureId: number, selections: Selection[], ni
return await getObservation(observation.id);
};

export const getObservationCount = async () => {
const db = useDB();
const [result] = await db.select({ count: count() }).from(observations);
return result.count;
};

export const getObservations = (pagination: Pagination) => {
const db = useDB();
return db.query.observations.findMany({
with: { images: true, capture: true, identifications: true },
orderBy: desc(observations.observedAt),
columns: {
moderated: false
},
limit: pagination.size,
offset: (pagination.page - 1) * pagination.size
});
};

const scaleBoundingBox = (boundingBox: BoundingBox, width: number, height: number) => {
return {
...boundingBox,
Expand All @@ -88,9 +107,6 @@ const scaleBoundingBox = (boundingBox: BoundingBox, width: number, height: numbe
height: Math.round(boundingBox.height * height)
};
};
/*
`ffmpeg -accurate_seek -ss {frame * 60} -i input.mp4 -frames:v 1 frame.png`
*/

export const getFrameFromVideo = async (video: TemporaryFile, stats: ffmpeg.FfprobeStream, timestamp: number) => {
const { storage } = useEnvironment();
Expand Down Expand Up @@ -119,6 +135,7 @@ import { randomUUID } from 'crypto';
import ffmpeg from 'fluent-ffmpeg';
import { writeFile } from 'fs/promises';
import { z } from 'zod';
import { Pagination } from '../../api/observation';
import { useEnvironment, useUser } from '../../utils/env/env';

export const extractFrameFromVideo = async (video: TemporaryFile, timestamp: number, stats: ffmpeg.FfprobeStream) => {
Expand Down
14 changes: 10 additions & 4 deletions census/api/src/services/points/achievement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,16 @@ const registry = {

export type Achievements = keyof typeof registry;

export const recordAchievement = async (action: Achievements, username: string) => {
export const recordAchievement = async (action: Achievements, username: string, immediate = false) => {
const details = registry[action];
if (!details) throw new Error(`Invalid action: ${action}`);
await addAchievement(action, username, details.points);
const db = useDB();
return await db.transaction(async tx =>
withTransaction(tx, async () => {
await addAchievement(action, username, details.points, immediate);
if (immediate) return await addPoints(username, details.points);
})
);
};

export const redeemAchievementAndAwardPoints = async (username: string, id: number) => {
Expand Down Expand Up @@ -66,9 +72,9 @@ export const revokeAchievement = async (id: number) => {
);
};

const addAchievement = async (action: Achievements, username: string, points: number) => {
const addAchievement = async (action: Achievements, username: string, points: number, immediate = false) => {
const db = useDB();
await db.insert(achievements).values({ type: action, username, points });
await db.insert(achievements).values({ type: action, username, points, redeemed: immediate });
};

const redeemAchievement = async (username: string, id: number) => {
Expand Down
Loading

0 comments on commit a00a075

Please sign in to comment.