Sart to expose modern skins via graphql

This commit is contained in:
Jordan Eldredge 2022-03-20 22:20:15 -07:00
parent 8c222d39c7
commit 72bd7a2a53
6 changed files with 206 additions and 11 deletions

View file

@ -0,0 +1,32 @@
import SkinModel from "../../data/SkinModel";
import { knex } from "../../db";
import ModernSkinResolver from "./resolvers/ModernSkinResolver";
export default class ModernSkinsConnection {
_first: number;
_offset: number;
constructor(first: number, offset: number) {
this._first = first;
this._offset = offset;
}
_getQuery() {
let query = knex("skins").where({ skin_type: 2 });
return query;
}
async count() {
const count = await this._getQuery().count("*", { as: "count" });
return count[0].count;
}
async nodes(_args, ctx) {
const skins = await this._getQuery()
.select()
.limit(this._first)
.offset(this._offset);
return skins.map((skin) => {
return new ModernSkinResolver(new SkinModel(ctx, skin));
});
}
}

View file

@ -0,0 +1,27 @@
import SkinModel from "../../../data/SkinModel";
import ArchiveFileResolver from "./ArchiveFileResolver";
import { NodeResolver, toId } from "./NodeResolver";
export default class ModernSkinResolver implements NodeResolver {
_model: SkinModel;
constructor(model: SkinModel) {
this._model = model;
}
__typename = "ModernSkin";
async id() {
return toId(this.__typename, this.md5());
}
md5() {
return this._model.getMd5();
}
filename() {
return this._model.getFileName();
}
download_url() {
return this._model.getSkinUrl();
}
async archive_files() {
const files = await this._model.getArchiveFiles();
return files.map((file) => new ArchiveFileResolver(file));
}
}

View file

@ -16,6 +16,8 @@ import ArchiveFileModel from "../../../data/ArchiveFileModel";
import ArchiveFileResolver from "./ArchiveFileResolver";
import DatabaseStatisticsResolver from "./DatabaseStatisticsResolver";
import { fromId } from "./NodeResolver";
import ModernSkinsConnection from "../ModernSkinsConnection";
import ModernSkinResolver from "./ModernSkinResolver";
// These keys are already in the web client, so they are not secret at all.
const client = algoliasearch("HQ9I5Z6IM5", "6466695ec3f624a5fccf46ec49680e51");
@ -33,6 +35,13 @@ class RootResolver extends MutationResolver {
}
return new SkinResolver(skin);
}
case "ModernSkin": {
const skin = await SkinModel.fromMd5(ctx, localId);
if (skin == null) {
return null;
}
return new ModernSkinResolver(skin);
}
}
return null;
}
@ -41,7 +50,11 @@ class RootResolver extends MutationResolver {
if (skin == null) {
return null;
}
return new SkinResolver(skin);
if (skin.getSkinType() === "MODERN") {
return new ModernSkinResolver(skin);
} else {
return new SkinResolver(skin);
}
}
async fetch_tweet_by_url({ url }, { ctx }) {
const tweet = await TweetModel.fromAnything(ctx, url);
@ -90,6 +103,14 @@ class RootResolver extends MutationResolver {
}
return new SkinsConnection(first, offset, sort, filter);
}
async modern_skins({ first, offset }) {
if (first > 1000) {
throw new Error("Maximum limit is 1000");
}
return new ModernSkinsConnection(first, offset);
}
async skin_to_review(_args, { ctx }) {
if (!ctx.authed()) {
return null;

View file

@ -7,10 +7,77 @@ interface Node {
id: ID!
}
"""
A Winamp skin. Could be modern or classic.
**Note**: At some point in the future, this might be renamed to `Skin`.
"""
interface GenericSkin {
"""
GraphQL ID of the skin
"""
id: ID!
"""
MD5 hash of the skin's file
"""
md5: String
"""
URL to download the skin
"""
download_url: String
"""
Filename of skin when uploaded to the Museum. Note: In some cases a skin
has been uploaded under multiple names. Here we just pick one.
"""
filename: String
"""
List of files contained within the skin's .wsz/.wal archive
"""
archive_files: [ArchiveFile]
}
"""
A "modern" Winamp skin. These skins use the `.wal` file extension and are free-form.
Most functionality in the Winamp Skin Museum is centered around "classic" skins,
which are currently called just `Skin` in this schema.
"""
type ModernSkin implements GenericSkin & Node {
"""
GraphQL ID of the skin
"""
id: ID!
"""
MD5 hash of the skin's file
"""
md5: String
"""
URL to download the skin
"""
download_url: String
"""
Filename of skin when uploaded to the Museum. Note: In some cases a skin
has been uploaded under multiple names. Here we just pick one.
"""
filename: String
"""
List of files contained within the skin's .wsz archive
"""
archive_files: [ArchiveFile]
}
"""
A classic Winamp skin
"""
type Skin implements Node {
type Skin implements GenericSkin & Node {
"""
GraphQL ID of the skin
"""
@ -89,6 +156,21 @@ type Skin implements Node {
reviews: [Review]
}
"""
A collection of "modern" Winamp skins
"""
type ModernSkinsConnection {
"""
The total number of skins matching the filter
"""
count: Int
"""
The list of skins
"""
nodes: [ModernSkin]
}
"""
The judgement made about a skin by a moderator
"""
@ -523,7 +605,7 @@ type Query {
"""
Get a skin by its MD5 hash
"""
fetch_skin_by_md5(md5: String!): Skin
fetch_skin_by_md5(md5: String!): GenericSkin
"""
Get a tweet by its URL
@ -552,7 +634,7 @@ type Query {
): InternetArchiveItem
"""
All skins in the database
All classic skins in the database
**Note:** We don't currently support combining sorting and filtering.
"""
@ -563,6 +645,11 @@ type Query {
filter: SkinsFilterOption
): SkinsConnection
"""
All modern skins in the database
"""
modern_skins(first: Int = 10, offset: Int = 0): ModernSkinsConnection
"""
Search the database using the Algolia search index used by the Museum.

View file

@ -1,4 +1,3 @@
import { getScreenshotUrl, getSkinUrl } from "./skins";
import {
TweetStatus,
SkinRow,
@ -27,7 +26,7 @@ export const IS_NOT_README =
/(genex\.txt)|(genexinfo\.txt)|(gen_gslyrics\.txt)|(region\.txt)|(pledit\.txt)|(viscolor\.txt)|(winampmb\.txt)|("gen_ex help\.txt)|(mbinner\.txt)$/i;
export default class SkinModel {
constructor(readonly ctx: UserContext, readonly row: SkinRow) {}
constructor(readonly ctx: UserContext, readonly row: SkinRow) { }
static async fromMd5(
ctx: UserContext,
@ -163,13 +162,16 @@ export default class SkinModel {
throw new Error(`Could not find file for skin with md5 ${this.getMd5()}`);
}
const filename = files[0].getFileName();
if (!filename.match(/\.(zip)|(wsz)$/i)) {
throw new Error("Expected filename to end with zip or wsz.");
if (!filename.match(/\.(zip)|(wsz)|(wal)$/i)) {
throw new Error("Expected filename to end with zip, wsz or wal.");
}
return filename;
}
async getScreenshotFileName(): Promise<string> {
if (this.getSkinType() === "MODERN") {
throw new Error("Modern skins do not have screenshots yet.");
}
const skinFilename = await this.getFileName();
return skinFilename.replace(/\.(wsz|zip)$/, ".png");
}
@ -197,16 +199,31 @@ export default class SkinModel {
}
getMuseumUrl(): string {
if (this.getSkinType() === "MODERN") {
throw new Error("Modern skins do not render in the museum.");
}
return `https://skins.webamp.org/skin/${this.row.md5}`;
}
getWebampUrl(): string {
if (this.getSkinType() === "MODERN") {
throw new Error("Modern skins do not render in Webamp.");
}
return `https://webamp.org?skinUrl=${this.getSkinUrl()}`;
}
getScreenshotUrl(): string {
return getScreenshotUrl(this.row.md5);
if (this.getSkinType() === "MODERN") {
throw new Error("Modern skins do not have screenshots.");
}
return Skins.getScreenshotUrl(this.row.md5);
}
getSkinUrl(): string {
return getSkinUrl(this.row.md5);
switch (this.getSkinType()) {
case "CLASSIC":
return Skins.getSkinUrl(this.row.md5);
case "MODERN":
return Skins.getModernSkinUrl(this.row.md5);
}
}
getAverageColor(): string {
@ -215,10 +232,11 @@ export default class SkinModel {
getBuffer = mem(async (): Promise<Buffer> => {
if (process.env.LOCAL_FILE_CACHE) {
const ext = this.getSkinType() === "CLASSIC" ? ".wsz" : ".wal";
const skinPath = path.join(
process.env.LOCAL_FILE_CACHE,
"skins",
this.getMd5() + ".wsz"
this.getMd5() + ext
);
return fs.readFile(skinPath);
} else {
@ -231,6 +249,9 @@ export default class SkinModel {
});
getScreenshotBuffer = mem(async (): Promise<Buffer> => {
if (this.getSkinType() === "MODERN") {
throw new Error("Modern skins do not have screenshots.");
}
const response = await fetch(this.getScreenshotUrl());
if (!response.ok) {
throw new Error(
@ -249,6 +270,9 @@ export default class SkinModel {
async withScreenshotTempFile(
cb: (file: string) => Promise<void>
): Promise<void> {
if (this.getSkinType() === "MODERN") {
throw new Error("Modern skins do not have screenshots.");
}
const screenshotFilename = await this.getScreenshotFileName();
return withUrlAsTempFile(this.getScreenshotUrl(), screenshotFilename, cb);
}

View file

@ -21,6 +21,10 @@ export function getSkinUrl(md5: string): string {
return `https://cdn.webampskins.org/skins/${md5}.wsz`;
}
export function getModernSkinUrl(md5: string): string {
return `https://cdn.webampskins.org/skins/${md5}.wal`;
}
export function getScreenshotUrl(md5: string): string {
return `https://cdn.webampskins.org/screenshots/${md5}.png`;
}