mirror of
https://github.com/captbaritone/webamp.git
synced 2026-01-23 10:15:31 +00:00
Schma parity between generated an Grats
This commit is contained in:
parent
59a1ed88db
commit
7c4e9258f0
16 changed files with 770 additions and 210 deletions
|
|
@ -3,7 +3,9 @@ import SkinModel from "../../data/SkinModel";
|
|||
import { knex } from "../../db";
|
||||
import ModernSkinResolver from "./resolvers/ModernSkinResolver";
|
||||
|
||||
/** @gqlType */
|
||||
/**
|
||||
* A collection of "modern" Winamp skins
|
||||
* @gqlType */
|
||||
export default class ModernSkinsConnection {
|
||||
_first: number;
|
||||
_offset: number;
|
||||
|
|
@ -16,14 +18,18 @@ export default class ModernSkinsConnection {
|
|||
return query;
|
||||
}
|
||||
|
||||
/** @gqlField */
|
||||
/**
|
||||
* The total number of skins matching the filter
|
||||
* @gqlField */
|
||||
async count(): Promise<Int> {
|
||||
const count = await this._getQuery().count("*", { as: "count" });
|
||||
return Number(count[0].count);
|
||||
}
|
||||
|
||||
/** @gqlField */
|
||||
async nodes(_args: never, ctx): Promise<ModernSkinResolver[]> {
|
||||
/**
|
||||
* The list of skins
|
||||
* @gqlField */
|
||||
async nodes(_args: never, ctx): Promise<Array<ModernSkinResolver | null>> {
|
||||
const skins = await this._getQuery()
|
||||
.select()
|
||||
.limit(this._first)
|
||||
|
|
|
|||
|
|
@ -36,7 +36,10 @@ async function getSkinMuseumPageFromCache(first: number, offset: number) {
|
|||
return skins;
|
||||
}
|
||||
|
||||
/** @gqlType */
|
||||
/**
|
||||
* A collection of classic Winamp skins
|
||||
* @gqlType
|
||||
*/
|
||||
export default class SkinsConnection {
|
||||
_first: number;
|
||||
_offset: number;
|
||||
|
|
@ -76,7 +79,10 @@ export default class SkinsConnection {
|
|||
return query;
|
||||
}
|
||||
|
||||
/** @gqlField */
|
||||
/**
|
||||
* The total number of skins matching the filter
|
||||
* @gqlField
|
||||
*/
|
||||
async count(): Promise<Int> {
|
||||
if (this._sort === "MUSEUM") {
|
||||
// This is the common case, so serve it from cache.
|
||||
|
|
@ -86,8 +92,11 @@ export default class SkinsConnection {
|
|||
return Number(count[0].count);
|
||||
}
|
||||
|
||||
/** @gqlField */
|
||||
async nodes(args: never, ctx): Promise<ISkin[]> {
|
||||
/**
|
||||
* The list of skins
|
||||
* @gqlField
|
||||
*/
|
||||
async nodes(args: never, ctx): Promise<Array<ISkin | null>> {
|
||||
if (this._sort === "MUSEUM") {
|
||||
if (this._filter) {
|
||||
throw new Error(
|
||||
|
|
|
|||
|
|
@ -6,7 +6,10 @@ import TweetResolver from "./resolvers/TweetResolver";
|
|||
/** @gqlEnum */
|
||||
export type TweetsSortOption = "LIKES" | "RETWEETS";
|
||||
|
||||
/** @gqlType */
|
||||
/**
|
||||
* A collection of tweets made by the @winampskins bot
|
||||
* @gqlType
|
||||
*/
|
||||
export default class TweetsConnection {
|
||||
_first: number;
|
||||
_offset: number;
|
||||
|
|
@ -41,7 +44,7 @@ export default class TweetsConnection {
|
|||
* The list of tweets
|
||||
* @gqlField
|
||||
*/
|
||||
async nodes(args: never, ctx): Promise<TweetResolver[]> {
|
||||
async nodes(args: never, ctx): Promise<Array<TweetResolver | null>> {
|
||||
const tweets = await this._getQuery()
|
||||
.select()
|
||||
.limit(this._first)
|
||||
|
|
|
|||
|
|
@ -7,89 +7,245 @@ directive @exported(filename: String!, functionName: String!) on FIELD_DEFINITIO
|
|||
|
||||
directive @methodName(name: String!) on FIELD_DEFINITION
|
||||
|
||||
"""A file found within a Winamp Skin's .wsz archive"""
|
||||
type ArchiveFile {
|
||||
"""
|
||||
The date on the file inside the archive. Given in simplified extended ISO
|
||||
format (ISO 8601).
|
||||
"""
|
||||
date: String
|
||||
"""The md5 hash of the file within the archive"""
|
||||
file_md5: String
|
||||
"""Filename of the file within the archive"""
|
||||
filename: String
|
||||
"""Is the file a directory?"""
|
||||
is_directory: Boolean
|
||||
"""
|
||||
The uncompressed size of the file in bytes.
|
||||
|
||||
**Note:** Will be `null` for directories
|
||||
"""
|
||||
size: Int
|
||||
"""The skin in which this file was found"""
|
||||
skin: Skin
|
||||
"""The content of the file, if it's a text file"""
|
||||
text_content: String
|
||||
"""
|
||||
A URL to download the file. **Note:** This is powered by a little
|
||||
serverless Cloudflare function which tries to exctact the file on the fly.
|
||||
It may not work for all files.
|
||||
"""
|
||||
url: String
|
||||
}
|
||||
|
||||
"""A classic Winamp skin"""
|
||||
type ClassicSkin implements Node & Skin {
|
||||
archive_files: [ArchiveFile!]
|
||||
"""List of files contained within the skin's .wsz archive"""
|
||||
archive_files: [ArchiveFile]
|
||||
"""String representation (rgb usually) of the skin's average color"""
|
||||
average_color: String
|
||||
"""URL to download the skin"""
|
||||
download_url: String
|
||||
filename(normalize_extension: Boolean = false): 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(
|
||||
"""
|
||||
If true, the the correct file extension (.wsz or .wal) will be .
|
||||
Otherwise, the original user-uploaded file extension will be used.
|
||||
"""
|
||||
normalize_extension: Boolean = false
|
||||
): String
|
||||
"""Does the skin include sprite sheets for the media library?"""
|
||||
has_media_library: Boolean
|
||||
"""GraphQL ID of the skin"""
|
||||
id: ID!
|
||||
"""The skin's "item" at archive.org"""
|
||||
internet_archive_item: InternetArchiveItem
|
||||
"""
|
||||
The date on which this skin was last updated in the Algolia search index.
|
||||
Given in simplified extended ISO format (ISO 8601).
|
||||
"""
|
||||
last_algolia_index_update_date: String
|
||||
"""MD5 hash of the skin's file"""
|
||||
md5: String
|
||||
"""URL of the skin on the Winamp Skin Museum"""
|
||||
museum_url: String
|
||||
"""Has the skin been flagged as "not safe for wrok"?"""
|
||||
nsfw: Boolean
|
||||
"""Text of the readme file extracted from the skin"""
|
||||
readme_text: String
|
||||
reviews: [Review!]
|
||||
"""
|
||||
Times that the skin has been reviewed either on the Museum's Tinder-style
|
||||
reivew page, or via the Discord bot.
|
||||
"""
|
||||
reviews: [Review]
|
||||
"""URL of a screenshot of the skin"""
|
||||
screenshot_url: String
|
||||
"""The number of transparent pixels rendered by the skin."""
|
||||
transparent_pixels: Int
|
||||
"""Has the skin been tweeted?"""
|
||||
tweeted: Boolean
|
||||
tweets: [Tweet!]
|
||||
"""List of @winampskins tweets that mentioned the skin."""
|
||||
tweets: [Tweet]
|
||||
"""URL of webamp.org with the skin loaded"""
|
||||
webamp_url: String
|
||||
}
|
||||
|
||||
"""Statistics about the contents of the Museum's database."""
|
||||
type DatabaseStatistics {
|
||||
"""
|
||||
The number of skins that have been approved for tweeting. This includes both
|
||||
tweeted and untweeted skins.
|
||||
|
||||
**Note:** Skins can be both approved and rejected by different users.
|
||||
"""
|
||||
approved_skins_count: Int
|
||||
"""
|
||||
The number of skins that have been marked as NSFW.
|
||||
|
||||
**Note:** Skins can be approved and rejected by different users.
|
||||
**Note:** Generally skins that have been marked NSFW are also marked as rejected.
|
||||
"""
|
||||
nsfw_skins_count: Int
|
||||
"""
|
||||
The number of skins that have been rejected for tweeting.
|
||||
|
||||
**Note:** Skins can be both approved and rejected by different users.
|
||||
**Note:** Generally skins that have been marked NSFW are also marked as rejected.
|
||||
"""
|
||||
rejected_skins_count: Int
|
||||
"""
|
||||
The number of skins that have been approved for tweeting, but not yet tweeted.
|
||||
"""
|
||||
tweetable_skins_count: Int
|
||||
"""
|
||||
The number of skins in the Museum that have been tweeted by @winampskins
|
||||
"""
|
||||
tweeted_skins_count: Int
|
||||
"""The total number of classic skins in the Museum's database"""
|
||||
unique_classic_skins_count: Int
|
||||
"""The number of skins that have never been reviewed."""
|
||||
unreviewed_skins_count: Int
|
||||
"""Skins uploads that have errored during processing."""
|
||||
uploads_in_error_state_count: Int
|
||||
"""
|
||||
Skins uplaods awaiting processing. This can happen when there are a large
|
||||
number of skin uplaods at the same time, or when the skin uploading processing
|
||||
pipeline gets stuck.
|
||||
"""
|
||||
uploads_pending_processing_count: Int
|
||||
"""
|
||||
Number of skins that have been uploaded to the Museum via the web interface.
|
||||
"""
|
||||
web_uploads_count: Int
|
||||
}
|
||||
|
||||
type InternetArchiveItem {
|
||||
"""The Internet Archive's unique identifier for this item"""
|
||||
identifier: String
|
||||
"""
|
||||
The date and time that we last scraped this item's metadata.
|
||||
**Note:** This field is temporary and will be removed in the future.
|
||||
The date format is just what we get from the database, and it's ambiguous.
|
||||
"""
|
||||
last_metadata_scrape_date_UNSTABLE: String
|
||||
"""URL to get the Internet Archive's metadata for this item in JSON form."""
|
||||
metadata_url: String
|
||||
"""
|
||||
Our cached version of the metadata avaliable at \`metadata_url\` (above)
|
||||
"""
|
||||
raw_metadata_json: String
|
||||
"""The skin that this item contains"""
|
||||
skin: Skin
|
||||
"""The URL where this item can be viewed on the Internet Archive"""
|
||||
url: String
|
||||
}
|
||||
|
||||
"""
|
||||
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 Node & Skin {
|
||||
archive_files: [ArchiveFile!]
|
||||
average_color: String
|
||||
"""List of files contained within the skin's .wsz archive"""
|
||||
archive_files: [ArchiveFile]
|
||||
average_color: String @deprecated(reason: "Needed for migration")
|
||||
"""URL to download the skin"""
|
||||
download_url: String
|
||||
filename(normalize_extension: Boolean = false): 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(
|
||||
"""
|
||||
If true, the the correct file extension (.wsz or .wal) will be .
|
||||
Otherwise, the original user-uploaded file extension will be used.
|
||||
"""
|
||||
normalize_extension: Boolean = false
|
||||
): String
|
||||
"""GraphQL ID of the skin"""
|
||||
id: ID!
|
||||
"""The skin's "item" at archive.org"""
|
||||
internet_archive_item: InternetArchiveItem
|
||||
"""MD5 hash of the skin's file"""
|
||||
md5: String
|
||||
museum_url: String
|
||||
nsfw: Boolean
|
||||
readme_text: String
|
||||
reviews: [Review!]
|
||||
screenshot_url: String
|
||||
museum_url: String @deprecated(reason: "Needed for migration")
|
||||
nsfw: Boolean @deprecated(reason: "Needed for migration")
|
||||
readme_text: String @deprecated(reason: "Needed for migration")
|
||||
"""
|
||||
Times that the skin has been reviewed either on the Museum's Tinder-style
|
||||
reivew page, or via the Discord bot.
|
||||
"""
|
||||
reviews: [Review]
|
||||
screenshot_url: String @deprecated(reason: "Needed for migration")
|
||||
"""Has the skin been tweeted?"""
|
||||
tweeted: Boolean
|
||||
tweets: [Tweet!]
|
||||
webamp_url: String
|
||||
"""List of @winampskins tweets that mentioned the skin."""
|
||||
tweets: [Tweet]
|
||||
webamp_url: String @deprecated(reason: "Needed for migration")
|
||||
}
|
||||
|
||||
"""A collection of "modern" Winamp skins"""
|
||||
type ModernSkinsConnection {
|
||||
"""The total number of skins matching the filter"""
|
||||
count: Int
|
||||
nodes: [ModernSkin!]
|
||||
"""The list of skins"""
|
||||
nodes: [ModernSkin]
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
"""
|
||||
Approve skin for tweeting
|
||||
|
||||
**Note:** Requires being logged in
|
||||
"""
|
||||
approve_skin(md5: String!): Boolean
|
||||
"""
|
||||
Mark a skin as NSFW
|
||||
|
||||
**Note:** Requires being logged in
|
||||
"""
|
||||
mark_skin_nsfw(md5: String!): Boolean
|
||||
"""
|
||||
Reject skin for tweeting
|
||||
|
||||
**Note:** Requires being logged in
|
||||
"""
|
||||
reject_skin(md5: String!): Boolean
|
||||
"""
|
||||
Request that an admin check if this skin is NSFW.
|
||||
Unlike other review mutaiton endpoints, this one does not require being logged
|
||||
in.
|
||||
"""
|
||||
request_nsfw_review_for_skin(md5: String!): Boolean
|
||||
send_feedback(email: String, message: String, url: String): Boolean
|
||||
"""
|
||||
Send a message to the admin of the site. Currently this appears in Discord.
|
||||
"""
|
||||
send_feedback(email: String, message: String!, url: String): Boolean
|
||||
"""Mutations for the upload flow"""
|
||||
upload: UploadMutations
|
||||
}
|
||||
|
||||
|
|
@ -105,11 +261,13 @@ interface Node {
|
|||
type Query {
|
||||
"""
|
||||
Fetch archive file by it's MD5 hash
|
||||
|
||||
Get information about a file found within a skin's wsz/wal/zip archive.
|
||||
"""
|
||||
fetch_archive_file_by_md5(md5: String!): ArchiveFile
|
||||
"""
|
||||
Get an archive.org item by its identifier. You can find this in the URL:
|
||||
|
||||
https://archive.org/details/<identifier>/
|
||||
"""
|
||||
fetch_internet_archive_item_by_identifier(identifier: String!): InternetArchiveItem
|
||||
|
|
@ -119,9 +277,11 @@ type Query {
|
|||
fetch_tweet_by_url(url: String!): Tweet
|
||||
"""The currently authenticated user, if any."""
|
||||
me: User
|
||||
"""All modern skins in the database"""
|
||||
modern_skins(first: Int = 10, offset: Int = 0): ModernSkinsConnection
|
||||
"""
|
||||
Get a globally unique object by its ID.
|
||||
|
||||
https://graphql.org/learn/global-object-identification/
|
||||
"""
|
||||
node(id: ID!): Node
|
||||
|
|
@ -130,17 +290,26 @@ type Query {
|
|||
|
||||
Useful for locating a particular skin.
|
||||
"""
|
||||
search_skins(first: Int = 10, offset: Int = 0, query: String!): [Skin!]
|
||||
search_skins(first: Int = 10, offset: Int = 0, query: String!): [Skin]
|
||||
"""A random skin that needs to be reviewed"""
|
||||
skin_to_review: Skin
|
||||
"""
|
||||
All classic skins in the database
|
||||
|
||||
**Note:** We don't currently support combining sorting and filtering.
|
||||
"""
|
||||
skins(filter: SkinsFilterOption, first: Int = 10, offset: Int = 0, sort: SkinsSortOption): SkinsConnection
|
||||
"""A namespace for statistics about the database"""
|
||||
statistics: DatabaseStatistics
|
||||
"""Tweets tweeted by @winampskins"""
|
||||
tweets(first: Int = 10, offset: Int = 0, sort: TweetsSortOption): TweetsConnection
|
||||
upload_statuses(ids: [String!]!): [SkinUpload!]
|
||||
"""Get the status of a batch of uploads by ids"""
|
||||
upload_statuses(ids: [String!]!): [SkinUpload]
|
||||
"""Get the status of a batch of uploads by md5s"""
|
||||
upload_statuses_by_md5(md5s: [String!]!): [SkinUpload!]
|
||||
upload_statuses_by_md5(md5s: [String!]!): [SkinUpload] @deprecated(reason: "Prefer `upload_statuses` instead, were we operate on ids.")
|
||||
}
|
||||
|
||||
"""The judgement made about a skin by a moderator"""
|
||||
enum Rating {
|
||||
APPROVED
|
||||
NSFW
|
||||
|
|
@ -155,41 +324,74 @@ type Review {
|
|||
"""The rating that the user gave the skin"""
|
||||
rating: Rating
|
||||
"""
|
||||
The user who made the review (if known). **Note:** In the early days we
|
||||
didn't track this, so many will be null.
|
||||
The user who made the review (if known). **Note:** In the early days we didn't
|
||||
track this, so many will be null.
|
||||
"""
|
||||
reviewer: String
|
||||
"""The skin that was reviewed"""
|
||||
skin: Skin
|
||||
}
|
||||
|
||||
"""
|
||||
A Winamp skin. Could be modern or classic.
|
||||
|
||||
**Note**: At some point in the future, this might be renamed to `Skin`.
|
||||
"""
|
||||
interface Skin {
|
||||
archive_files: [ArchiveFile!]
|
||||
average_color: String @deprecated(reason: "Needed for migration to new skin model")
|
||||
"""List of files contained within the skin's .wsz archive"""
|
||||
archive_files: [ArchiveFile]
|
||||
average_color: String @deprecated(reason: "Needed for migration")
|
||||
"""URL to download the skin"""
|
||||
download_url: String
|
||||
filename(normalize_extension: Boolean = false): 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(
|
||||
"""
|
||||
If true, the the correct file extension (.wsz or .wal) will be .
|
||||
Otherwise, the original user-uploaded file extension will be used.
|
||||
"""
|
||||
normalize_extension: Boolean = false
|
||||
): String
|
||||
"""GraphQL ID of the skin"""
|
||||
id: ID!
|
||||
"""The skin's "item" at archive.org"""
|
||||
internet_archive_item: InternetArchiveItem
|
||||
"""MD5 hash of the skin's file"""
|
||||
md5: String
|
||||
museum_url: String @deprecated(reason: "Needed for migration to new skin model")
|
||||
nsfw: Boolean @deprecated(reason: "Needed for migration to new skin model")
|
||||
readme_text: String @deprecated(reason: "Needed for migration to new skin model")
|
||||
reviews: [Review!]
|
||||
screenshot_url: String @deprecated(reason: "Needed for migration to new skin model")
|
||||
museum_url: String @deprecated(reason: "Needed for migration")
|
||||
nsfw: Boolean @deprecated(reason: "Needed for migration")
|
||||
readme_text: String @deprecated(reason: "Needed for migration")
|
||||
"""
|
||||
Times that the skin has been reviewed either on the Museum's Tinder-style
|
||||
reivew page, or via the Discord bot.
|
||||
"""
|
||||
reviews: [Review]
|
||||
screenshot_url: String @deprecated(reason: "Needed for migration")
|
||||
"""Has the skin been tweeted?"""
|
||||
tweeted: Boolean
|
||||
tweets: [Tweet!]
|
||||
webamp_url: String @deprecated(reason: "Needed for migration to new skin model")
|
||||
"""List of @winampskins tweets that mentioned the skin."""
|
||||
tweets: [Tweet]
|
||||
webamp_url: String @deprecated(reason: "Needed for migration")
|
||||
}
|
||||
|
||||
"""Information about an attempt to upload a skin to the Museum."""
|
||||
type SkinUpload {
|
||||
id: String
|
||||
"""
|
||||
Skin that was uploaded. **Note:** This is null if the skin has not yet been
|
||||
fully processed. (status == ARCHIVED)
|
||||
"""
|
||||
skin: Skin
|
||||
status: SkinUploadStatus
|
||||
"""Md5 hash given when requesting the upload URL."""
|
||||
upload_md5: String
|
||||
}
|
||||
|
||||
"""
|
||||
The current status of a pending upload.
|
||||
|
||||
**Note:** Expect more values here as we try to be more transparent about
|
||||
the status of a pending uploads.
|
||||
"""
|
||||
|
|
@ -201,9 +403,12 @@ enum SkinUploadStatus {
|
|||
URL_REQUESTED
|
||||
}
|
||||
|
||||
"""A collection of classic Winamp skins"""
|
||||
type SkinsConnection {
|
||||
"""The total number of skins matching the filter"""
|
||||
count: Int
|
||||
nodes: [Skin!]
|
||||
"""The list of skins"""
|
||||
nodes: [Skin]
|
||||
}
|
||||
|
||||
enum SkinsFilterOption {
|
||||
|
|
@ -217,18 +422,32 @@ enum SkinsSortOption {
|
|||
MUSEUM
|
||||
}
|
||||
|
||||
"""A tweet made by @winampskins mentioning a Winamp skin"""
|
||||
type Tweet {
|
||||
"""
|
||||
Number of likes the tweet has received. Updated nightly. (Note: Recent likes on older tweets may not be reflected here)
|
||||
"""
|
||||
likes: Int
|
||||
"""
|
||||
Number of retweets the tweet has received. Updated nightly. (Note: Recent retweets on older tweets may not be reflected here)
|
||||
"""
|
||||
retweets: Int
|
||||
"""The skin featured in this Tweet"""
|
||||
skin: Skin
|
||||
"""
|
||||
URL of the tweet. **Note:** Early on in the bot's life we just recorded
|
||||
_which_ skins were tweeted, not any info about the actual tweet. This means we
|
||||
don't always know the URL of the tweet.
|
||||
"""
|
||||
url: String
|
||||
}
|
||||
|
||||
"""A collection of tweets made by the @winampskins bot"""
|
||||
type TweetsConnection {
|
||||
"""The total number of tweets"""
|
||||
count: Int
|
||||
"""The list of tweets"""
|
||||
nodes: [Tweet!]
|
||||
nodes: [Tweet]
|
||||
}
|
||||
|
||||
enum TweetsSortOption {
|
||||
|
|
@ -236,17 +455,38 @@ enum TweetsSortOption {
|
|||
RETWEETS
|
||||
}
|
||||
|
||||
"""
|
||||
Mutations for the upload flow
|
||||
|
||||
1. The user finds the md5 hash of their local files.
|
||||
2. (`get_upload_urls`) The user requests upload URLs for each of their files.
|
||||
3. The server returns upload URLs for each of their files which are not already in the collection.
|
||||
4. The user uploads each of their files to the URLs returned in step 3.
|
||||
5. (`report_skin_uploaded`) The user notifies the server that they're done uploading.
|
||||
6. (TODO) The user polls for the status of their uploads.
|
||||
"""
|
||||
type UploadMutations {
|
||||
get_upload_urls(files: [UploadUrlRequest!]!): [UploadUrl!]
|
||||
"""
|
||||
Get a (possibly incompelte) list of UploadUrls for each of the files. If an
|
||||
UploadUrl is not returned for a given hash, it means the file is already in
|
||||
the collection.
|
||||
"""
|
||||
get_upload_urls(files: [UploadUrlRequest!]!): [UploadUrl]
|
||||
"""Notify the server that the user is done uploading."""
|
||||
report_skin_uploaded(id: String!, md5: String!): Boolean
|
||||
}
|
||||
|
||||
"""
|
||||
A URL that the client can use to upload a skin to S3, and then notify the server
|
||||
when they're done.
|
||||
"""
|
||||
type UploadUrl {
|
||||
id: String
|
||||
md5: String
|
||||
url: String
|
||||
}
|
||||
|
||||
"""Input object used for a user to request an UploadUrl"""
|
||||
input UploadUrlRequest {
|
||||
filename: String!
|
||||
md5: String!
|
||||
|
|
|
|||
|
|
@ -4,45 +4,77 @@ import SkinModel from "../../../data/SkinModel";
|
|||
import { ISkin } from "./CommonSkinResolver";
|
||||
import SkinResolver from "./SkinResolver";
|
||||
|
||||
/** @gqlType ArchiveFile */
|
||||
/**
|
||||
* A file found within a Winamp Skin's .wsz archive
|
||||
* @gqlType ArchiveFile
|
||||
*/
|
||||
export default class ArchiveFileResolver {
|
||||
_model: ArchiveFileModel;
|
||||
constructor(model: ArchiveFileModel) {
|
||||
this._model = model;
|
||||
}
|
||||
|
||||
/** @gqlField */
|
||||
/**
|
||||
* Filename of the file within the archive
|
||||
* @gqlField
|
||||
*/
|
||||
filename(): string {
|
||||
return this._model.getFileName();
|
||||
}
|
||||
/** @gqlField */
|
||||
/**
|
||||
* A URL to download the file. **Note:** This is powered by a little
|
||||
* serverless Cloudflare function which tries to exctact the file on the fly.
|
||||
* It may not work for all files.
|
||||
* @gqlField
|
||||
*/
|
||||
url(): string {
|
||||
return this._model.getUrl();
|
||||
}
|
||||
/** @gqlField */
|
||||
/**
|
||||
* The md5 hash of the file within the archive
|
||||
* @gqlField
|
||||
*/
|
||||
file_md5(): string {
|
||||
return this._model.getFileMd5();
|
||||
}
|
||||
/** @gqlField */
|
||||
/**
|
||||
* The uncompressed size of the file in bytes.
|
||||
*
|
||||
* **Note:** Will be `null` for directories
|
||||
* @gqlField
|
||||
*/
|
||||
size(): Promise<Int | null> {
|
||||
return this._model.getFileSize();
|
||||
}
|
||||
/** @gqlField */
|
||||
/**
|
||||
* The content of the file, if it's a text file
|
||||
* @gqlField
|
||||
*/
|
||||
text_content(): Promise<string | null> {
|
||||
return this._model.getTextContent();
|
||||
}
|
||||
/** @gqlField */
|
||||
/**
|
||||
* Is the file a directory?
|
||||
* @gqlField
|
||||
*/
|
||||
is_directory(): boolean {
|
||||
return this._model.getIsDirectory();
|
||||
}
|
||||
|
||||
/** @gqlField */
|
||||
/**
|
||||
* The skin in which this file was found
|
||||
* @gqlField
|
||||
*/
|
||||
async skin(_: never, { ctx }): Promise<ISkin | null> {
|
||||
const model = await SkinModel.fromMd5Assert(ctx, this._model.getMd5());
|
||||
return SkinResolver.fromModel(model);
|
||||
}
|
||||
|
||||
/** @gqlField */
|
||||
/**
|
||||
* The date on the file inside the archive. Given in simplified extended ISO
|
||||
* format (ISO 8601).
|
||||
* @gqlField
|
||||
*/
|
||||
date(): string {
|
||||
return this._model.getFileDate().toISOString();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,23 +7,31 @@ import TweetResolver from "./TweetResolver";
|
|||
import ArchiveFileResolver from "./ArchiveFileResolver";
|
||||
import InternetArchiveItemResolver from "./InternetArchiveItemResolver";
|
||||
|
||||
/** @gqlType ClassicSkin */
|
||||
/**
|
||||
* A classic Winamp skin
|
||||
* @gqlType ClassicSkin */
|
||||
export default class ClassicSkinResolver
|
||||
extends CommonSkinResolver
|
||||
implements NodeResolver, ISkin
|
||||
{
|
||||
__typename = "ClassicSkin";
|
||||
/**
|
||||
* GraphQL ID of the skin
|
||||
* @gqlField
|
||||
* @killsParentOnException
|
||||
*/
|
||||
id(): ID {
|
||||
return toId(this.__typename, this.md5());
|
||||
}
|
||||
/** @gqlField */
|
||||
/**
|
||||
* 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.
|
||||
* @gqlField */
|
||||
async filename({
|
||||
normalize_extension = false,
|
||||
}: {
|
||||
/** If true, the the correct file extension (.wsz or .wal) will be .
|
||||
Otherwise, the original user-uploaded file extension will be used. */
|
||||
normalize_extension?: boolean;
|
||||
}): Promise<string> {
|
||||
const filename = await this._model.getFileName();
|
||||
|
|
@ -33,67 +41,99 @@ export default class ClassicSkinResolver
|
|||
return filename;
|
||||
}
|
||||
|
||||
/** @gqlField */
|
||||
/**
|
||||
* MD5 hash of the skin's file
|
||||
* @gqlField */
|
||||
md5(): string {
|
||||
return super.md5();
|
||||
}
|
||||
/** @gqlField */
|
||||
/**
|
||||
* URL to download the skin
|
||||
* @gqlField */
|
||||
download_url(): string {
|
||||
return super.download_url();
|
||||
}
|
||||
/** @gqlField */
|
||||
/**
|
||||
* Has the skin been tweeted?
|
||||
* @gqlField */
|
||||
tweeted(): Promise<boolean> {
|
||||
return super.tweeted();
|
||||
}
|
||||
|
||||
/** @gqlField */
|
||||
tweets(): Promise<TweetResolver[]> {
|
||||
/**
|
||||
* List of @winampskins tweets that mentioned the skin.
|
||||
* @gqlField */
|
||||
tweets(): Promise<Array<TweetResolver | null>> {
|
||||
return super.tweets();
|
||||
}
|
||||
|
||||
/** @gqlField */
|
||||
archive_files(): Promise<ArchiveFileResolver[]> {
|
||||
/**
|
||||
* List of files contained within the skin's .wsz archive
|
||||
* @gqlField */
|
||||
archive_files(): Promise<Array<ArchiveFileResolver | null>> {
|
||||
return super.archive_files();
|
||||
}
|
||||
|
||||
/** @gqlField */
|
||||
/**
|
||||
* The skin's "item" at archive.org
|
||||
* @gqlField */
|
||||
internet_archive_item(): Promise<InternetArchiveItemResolver | null> {
|
||||
return super.internet_archive_item();
|
||||
}
|
||||
/** @gqlField */
|
||||
/**
|
||||
* URL of the skin on the Winamp Skin Museum
|
||||
* @gqlField */
|
||||
museum_url(): string {
|
||||
return this._model.getMuseumUrl();
|
||||
}
|
||||
/** @gqlField */
|
||||
/**
|
||||
* URL of webamp.org with the skin loaded
|
||||
* @gqlField */
|
||||
webamp_url(): string {
|
||||
return this._model.getWebampUrl();
|
||||
}
|
||||
/** @gqlField */
|
||||
/**
|
||||
* URL of a screenshot of the skin
|
||||
* @gqlField */
|
||||
screenshot_url(): string {
|
||||
return this._model.getScreenshotUrl();
|
||||
}
|
||||
/** @gqlField */
|
||||
/**
|
||||
* Text of the readme file extracted from the skin
|
||||
* @gqlField */
|
||||
readme_text(): Promise<string | null> {
|
||||
return this._model.getReadme();
|
||||
}
|
||||
/** @gqlField */
|
||||
/**
|
||||
* Has the skin been flagged as "not safe for wrok"?
|
||||
* @gqlField */
|
||||
nsfw(): Promise<boolean> {
|
||||
return this._model.getIsNsfw();
|
||||
}
|
||||
/** @gqlField */
|
||||
/**
|
||||
* String representation (rgb usually) of the skin's average color
|
||||
* @gqlField */
|
||||
average_color(): string {
|
||||
return this._model.getAverageColor();
|
||||
}
|
||||
/** @gqlField */
|
||||
/**
|
||||
* Does the skin include sprite sheets for the media library?
|
||||
* @gqlField */
|
||||
has_media_library(): Promise<boolean> {
|
||||
return this._model.hasMediaLibrary();
|
||||
}
|
||||
/** @gqlField */
|
||||
async reviews(): Promise<ReviewResolver[]> {
|
||||
/**
|
||||
* Times that the skin has been reviewed either on the Museum's Tinder-style
|
||||
* reivew page, or via the Discord bot.
|
||||
* @gqlField */
|
||||
async reviews(): Promise<Array<ReviewResolver | null>> {
|
||||
const reviews = await this._model.getReviews();
|
||||
return reviews.map((row) => new ReviewResolver(row));
|
||||
}
|
||||
/** @gqlField */
|
||||
/**
|
||||
* The date on which this skin was last updated in the Algolia search index.
|
||||
* Given in simplified extended ISO format (ISO 8601).
|
||||
* @gqlField */
|
||||
async last_algolia_index_update_date(): Promise<string | null> {
|
||||
const updates = await this._model.getAlgoliaIndexUpdates(1);
|
||||
if (updates.length < 1) {
|
||||
|
|
@ -102,7 +142,9 @@ export default class ClassicSkinResolver
|
|||
const update = updates[0];
|
||||
return new Date(update.update_timestamp * 1000).toISOString();
|
||||
}
|
||||
/** @gqlField */
|
||||
/**
|
||||
* The number of transparent pixels rendered by the skin.
|
||||
* @gqlField */
|
||||
transparent_pixels(): Promise<Int> {
|
||||
return this._model.transparentPixels();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,77 +5,109 @@ import InternetArchiveItemResolver from "./InternetArchiveItemResolver";
|
|||
import ReviewResolver from "./ReviewResolver";
|
||||
import TweetResolver from "./TweetResolver";
|
||||
|
||||
/** @gqlInterface Skin */
|
||||
/**
|
||||
* A Winamp skin. Could be modern or classic.
|
||||
*
|
||||
* **Note**: At some point in the future, this might be renamed to `Skin`.
|
||||
* @gqlInterface Skin
|
||||
*/
|
||||
export interface ISkin {
|
||||
__typename: string;
|
||||
/**
|
||||
* GraphQL ID of the skin
|
||||
* @gqlField
|
||||
* @killsParentOnException
|
||||
*/
|
||||
id(): ID;
|
||||
|
||||
/** @gqlField */
|
||||
/**
|
||||
* 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.
|
||||
* @gqlField
|
||||
*/
|
||||
filename({
|
||||
normalize_extension = false,
|
||||
}: {
|
||||
/**
|
||||
* If true, the the correct file extension (.wsz or .wal) will be .
|
||||
* Otherwise, the original user-uploaded file extension will be used.
|
||||
*/
|
||||
normalize_extension?: boolean;
|
||||
}): Promise<string>;
|
||||
|
||||
/** @gqlField */
|
||||
/**
|
||||
* MD5 hash of the skin's file
|
||||
* @gqlField
|
||||
*/
|
||||
md5(): string;
|
||||
|
||||
/** @gqlField */
|
||||
/**
|
||||
* URL to download the skin
|
||||
* @gqlField
|
||||
*/
|
||||
download_url(): string;
|
||||
|
||||
/** @gqlField */
|
||||
/**
|
||||
* Has the skin been tweeted?
|
||||
* @gqlField
|
||||
*/
|
||||
tweeted(): Promise<boolean>;
|
||||
|
||||
/** @gqlField */
|
||||
tweets(): Promise<TweetResolver[]>;
|
||||
/**
|
||||
* List of @winampskins tweets that mentioned the skin.
|
||||
* @gqlField
|
||||
*/
|
||||
tweets(): Promise<Array<TweetResolver | null>>;
|
||||
|
||||
/** @gqlField */
|
||||
archive_files(): Promise<ArchiveFileResolver[]>;
|
||||
/**
|
||||
* List of files contained within the skin's .wsz archive
|
||||
* @gqlField
|
||||
*/
|
||||
archive_files(): Promise<Array<ArchiveFileResolver | null>>;
|
||||
|
||||
/** @gqlField */
|
||||
/**
|
||||
* The skin's "item" at archive.org
|
||||
* @gqlField
|
||||
*/
|
||||
internet_archive_item(): Promise<InternetArchiveItemResolver | null>;
|
||||
|
||||
/** @gqlField */
|
||||
reviews(): Promise<ReviewResolver[] | null>;
|
||||
/**
|
||||
* Times that the skin has been reviewed either on the Museum's Tinder-style
|
||||
* reivew page, or via the Discord bot.
|
||||
* @gqlField
|
||||
*/
|
||||
reviews(): Promise<Array<ReviewResolver | null>>;
|
||||
|
||||
/**
|
||||
* @gqlField
|
||||
* @deprecated Needed for migration to new skin model
|
||||
* @deprecated Needed for migration
|
||||
*/
|
||||
museum_url(): string | null;
|
||||
|
||||
/**
|
||||
* @gqlField
|
||||
* @deprecated Needed for migration to new skin model
|
||||
* @deprecated Needed for migration
|
||||
*/
|
||||
webamp_url(): string | null;
|
||||
|
||||
/**
|
||||
* @gqlField
|
||||
* @deprecated Needed for migration to new skin model
|
||||
*/
|
||||
* @deprecated Needed for migration */
|
||||
screenshot_url(): string | null;
|
||||
|
||||
/**
|
||||
* @gqlField
|
||||
* @deprecated Needed for migration to new skin model
|
||||
*/
|
||||
* @deprecated Needed for migration */
|
||||
readme_text(): Promise<string | null>;
|
||||
|
||||
/**
|
||||
* @gqlField
|
||||
* @deprecated Needed for migration to new skin model
|
||||
*/
|
||||
* @deprecated Needed for migration */
|
||||
nsfw(): Promise<boolean | null>;
|
||||
|
||||
/**
|
||||
* @gqlField
|
||||
* @deprecated Needed for migration to new skin model
|
||||
*/
|
||||
* @deprecated Needed for migration */
|
||||
average_color(): string | null;
|
||||
}
|
||||
|
||||
|
|
@ -94,11 +126,11 @@ export default class CommonSkinResolver {
|
|||
tweeted(): Promise<boolean> {
|
||||
return this._model.tweeted();
|
||||
}
|
||||
async tweets(): Promise<TweetResolver[]> {
|
||||
async tweets(): Promise<Array<TweetResolver | null>> {
|
||||
const tweets = await this._model.getTweets();
|
||||
return tweets.map((tweetModel) => new TweetResolver(tweetModel));
|
||||
}
|
||||
async archive_files(): Promise<ArchiveFileResolver[]> {
|
||||
async archive_files(): Promise<Array<ArchiveFileResolver | null>> {
|
||||
const files = await this._model.getArchiveFiles();
|
||||
return files.map((file) => new ArchiveFileResolver(file));
|
||||
}
|
||||
|
|
@ -110,7 +142,7 @@ export default class CommonSkinResolver {
|
|||
return new InternetArchiveItemResolver(item);
|
||||
}
|
||||
|
||||
async reviews(): Promise<ReviewResolver[] | null> {
|
||||
async reviews(): Promise<Array<ReviewResolver | null>> {
|
||||
const reviews = await this._model.getReviews();
|
||||
return reviews.map((row) => new ReviewResolver(row));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,45 +1,89 @@
|
|||
import { Int } from "grats";
|
||||
import * as Skins from "../../../data/skins";
|
||||
|
||||
/** @gqlType DatabaseStatistics */
|
||||
/**
|
||||
* Statistics about the contents of the Museum's database.
|
||||
* @gqlType DatabaseStatistics
|
||||
*/
|
||||
export default class DatabaseStatisticsResolver {
|
||||
/** @gqlField */
|
||||
/**
|
||||
* The total number of classic skins in the Museum's database
|
||||
* @gqlField
|
||||
*/
|
||||
unique_classic_skins_count(): Promise<Int> {
|
||||
return Skins.getClassicSkinCount();
|
||||
}
|
||||
/** @gqlField */
|
||||
/**
|
||||
* The number of skins in the Museum that have been tweeted by @winampskins
|
||||
* @gqlField
|
||||
*/
|
||||
tweeted_skins_count(): Promise<Int> {
|
||||
return Skins.getTweetedSkinCount();
|
||||
}
|
||||
/** @gqlField */
|
||||
/**
|
||||
* The number of skins that have been approved for tweeting. This includes both
|
||||
* tweeted and untweeted skins.
|
||||
*
|
||||
* **Note:** Skins can be both approved and rejected by different users.
|
||||
* @gqlField
|
||||
*/
|
||||
approved_skins_count(): Promise<Int> {
|
||||
return Skins.getApprovedSkinCount();
|
||||
}
|
||||
/** @gqlField */
|
||||
/**
|
||||
* The number of skins that have been rejected for tweeting.
|
||||
*
|
||||
* **Note:** Skins can be both approved and rejected by different users.
|
||||
* **Note:** Generally skins that have been marked NSFW are also marked as rejected.
|
||||
* @gqlField
|
||||
*/
|
||||
rejected_skins_count(): Promise<Int> {
|
||||
return Skins.getRejectedSkinCount();
|
||||
}
|
||||
/** @gqlField */
|
||||
/**
|
||||
* The number of skins that have been marked as NSFW.
|
||||
*
|
||||
* **Note:** Skins can be approved and rejected by different users.
|
||||
* **Note:** Generally skins that have been marked NSFW are also marked as rejected.
|
||||
* @gqlField
|
||||
*/
|
||||
nsfw_skins_count(): Promise<Int> {
|
||||
return Skins.getNsfwSkinCount();
|
||||
}
|
||||
/** @gqlField */
|
||||
/**
|
||||
* The number of skins that have never been reviewed.
|
||||
* @gqlField
|
||||
*/
|
||||
unreviewed_skins_count(): Promise<Int> {
|
||||
return Skins.getUnreviewedSkinCount();
|
||||
}
|
||||
/** @gqlField */
|
||||
/**
|
||||
* The number of skins that have been approved for tweeting, but not yet tweeted.
|
||||
* @gqlField
|
||||
*/
|
||||
tweetable_skins_count(): Promise<Int> {
|
||||
return Skins.getTweetableSkinCount();
|
||||
}
|
||||
/** @gqlField */
|
||||
/**
|
||||
* Skins uplaods awaiting processing. This can happen when there are a large
|
||||
* number of skin uplaods at the same time, or when the skin uploading processing
|
||||
* pipeline gets stuck.
|
||||
* @gqlField
|
||||
*/
|
||||
uploads_pending_processing_count(): Promise<Int> {
|
||||
return Skins.getUploadsAwaitingProcessingCount();
|
||||
}
|
||||
/** @gqlField */
|
||||
/**
|
||||
* Skins uploads that have errored during processing.
|
||||
* @gqlField
|
||||
*/
|
||||
uploads_in_error_state_count(): Promise<Int> {
|
||||
return Skins.getUploadsErroredCount();
|
||||
}
|
||||
/** @gqlField */
|
||||
/**
|
||||
* Number of skins that have been uploaded to the Museum via the web interface.
|
||||
* @gqlField
|
||||
*/
|
||||
web_uploads_count(): Promise<Int> {
|
||||
return Skins.getWebUploadsCount();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,29 +8,49 @@ export default class InternetArchiveItemResolver {
|
|||
constructor(model: IaItemModel) {
|
||||
this._model = model;
|
||||
}
|
||||
/** @gqlField */
|
||||
/**
|
||||
* The Internet Archive's unique identifier for this item
|
||||
* @gqlField
|
||||
*/
|
||||
identifier(): string {
|
||||
return this._model.getIdentifier();
|
||||
}
|
||||
/** @gqlField */
|
||||
/**
|
||||
* The URL where this item can be viewed on the Internet Archive
|
||||
* @gqlField
|
||||
*/
|
||||
url(): string {
|
||||
return this._model.getUrl();
|
||||
}
|
||||
/** @gqlField */
|
||||
/**
|
||||
* URL to get the Internet Archive's metadata for this item in JSON form.
|
||||
* @gqlField
|
||||
*/
|
||||
metadata_url(): string {
|
||||
return this._model.getMetadataUrl();
|
||||
}
|
||||
|
||||
/** @gqlField */
|
||||
/**
|
||||
* The date and time that we last scraped this item's metadata.
|
||||
* **Note:** This field is temporary and will be removed in the future.
|
||||
* The date format is just what we get from the database, and it's ambiguous.
|
||||
* @gqlField
|
||||
*/
|
||||
last_metadata_scrape_date_UNSTABLE(): string | null {
|
||||
return this._model.getMetadataTimestamp();
|
||||
}
|
||||
|
||||
/** @gqlField */
|
||||
/**
|
||||
* Our cached version of the metadata avaliable at \`metadata_url\` (above)
|
||||
* @gqlField
|
||||
*/
|
||||
raw_metadata_json(): string | null {
|
||||
return this._model.getMetadataJSON();
|
||||
}
|
||||
/** @gqlField */
|
||||
/**
|
||||
* The skin that this item contains
|
||||
* @gqlField
|
||||
*/
|
||||
async skin(): Promise<ISkin | null> {
|
||||
const skin = await this._model.getSkin();
|
||||
if (skin == null) {
|
||||
|
|
|
|||
|
|
@ -8,17 +8,27 @@ import InternetArchiveItemResolver from "./InternetArchiveItemResolver";
|
|||
import ArchiveFileResolver from "./ArchiveFileResolver";
|
||||
import TweetResolver from "./TweetResolver";
|
||||
|
||||
/** @gqlType ModernSkin */
|
||||
/**
|
||||
* 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.
|
||||
* @gqlType ModernSkin */
|
||||
export default class ModernSkinResolver
|
||||
extends CommonSkinResolver
|
||||
implements NodeResolver, ISkin
|
||||
{
|
||||
_model: SkinModel;
|
||||
__typename = "ModernSkin";
|
||||
/** @gqlField */
|
||||
/**
|
||||
* 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.
|
||||
* @gqlField */
|
||||
async filename({
|
||||
normalize_extension = false,
|
||||
}: {
|
||||
/** If true, the the correct file extension (.wsz or .wal) will be .
|
||||
Otherwise, the original user-uploaded file extension will be used. */
|
||||
normalize_extension?: boolean;
|
||||
}): Promise<string> {
|
||||
const filename = await this._model.getFileName();
|
||||
|
|
@ -31,68 +41,95 @@ export default class ModernSkinResolver
|
|||
/* TODO: Get all of these from the parent class/interface */
|
||||
|
||||
/**
|
||||
* GraphQL ID of the skin
|
||||
* @gqlField
|
||||
* @killsParentOnException
|
||||
*/
|
||||
* @killsParentOnException */
|
||||
id(): ID {
|
||||
return toId(this.__typename, this.md5());
|
||||
}
|
||||
|
||||
/** @gqlField */
|
||||
/**
|
||||
* MD5 hash of the skin's file
|
||||
* @gqlField */
|
||||
md5(): string {
|
||||
return super.md5();
|
||||
}
|
||||
|
||||
/** @gqlField */
|
||||
/**
|
||||
* URL to download the skin
|
||||
* @gqlField */
|
||||
download_url(): string {
|
||||
return super.download_url();
|
||||
}
|
||||
/** @gqlField */
|
||||
/**
|
||||
* Has the skin been tweeted?
|
||||
* @gqlField */
|
||||
tweeted(): Promise<boolean> {
|
||||
return super.tweeted();
|
||||
}
|
||||
/** @gqlField */
|
||||
async tweets(): Promise<TweetResolver[]> {
|
||||
/**
|
||||
* List of @winampskins tweets that mentioned the skin.
|
||||
* @gqlField */
|
||||
async tweets(): Promise<Array<TweetResolver | null>> {
|
||||
return super.tweets();
|
||||
}
|
||||
|
||||
/** @gqlField */
|
||||
async archive_files(): Promise<ArchiveFileResolver[]> {
|
||||
/**
|
||||
* List of files contained within the skin's .wsz archive
|
||||
* @gqlField */
|
||||
async archive_files(): Promise<Array<ArchiveFileResolver | null>> {
|
||||
return super.archive_files();
|
||||
}
|
||||
|
||||
/** @gqlField */
|
||||
/**
|
||||
* The skin's "item" at archive.org
|
||||
* @gqlField */
|
||||
async internet_archive_item(): Promise<InternetArchiveItemResolver | null> {
|
||||
return super.internet_archive_item();
|
||||
}
|
||||
|
||||
/** @gqlField */
|
||||
async reviews(): Promise<ReviewResolver[] | null> {
|
||||
/**
|
||||
* Times that the skin has been reviewed either on the Museum's Tinder-style
|
||||
* reivew page, or via the Discord bot.
|
||||
* @gqlField */
|
||||
async reviews(): Promise<Array<ReviewResolver | null>> {
|
||||
return super.reviews();
|
||||
}
|
||||
|
||||
/** @gqlField */
|
||||
/**
|
||||
* @gqlField
|
||||
* @deprecated Needed for migration */
|
||||
museum_url(): string | null {
|
||||
return null;
|
||||
}
|
||||
/** @gqlField */
|
||||
/**
|
||||
* @gqlField
|
||||
* @deprecated Needed for migration */
|
||||
webamp_url(): string | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @gqlField */
|
||||
/**
|
||||
* @gqlField
|
||||
* @deprecated Needed for migration */
|
||||
screenshot_url(): string | null {
|
||||
return null;
|
||||
}
|
||||
/** @gqlField */
|
||||
/**
|
||||
* @gqlField
|
||||
* @deprecated Needed for migration */
|
||||
async readme_text(): Promise<string | null> {
|
||||
return null;
|
||||
}
|
||||
/** @gqlField */
|
||||
/**
|
||||
* @gqlField
|
||||
* @deprecated Needed for migration */
|
||||
async nsfw(): Promise<boolean | null> {
|
||||
return null;
|
||||
}
|
||||
/** @gqlField */
|
||||
/**
|
||||
* @gqlField
|
||||
* @deprecated Needed for migration */
|
||||
average_color(): string | null {
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,11 @@ import * as Skins from "../../../data/skins";
|
|||
import { processUserUploads } from "../../processUserUploads";
|
||||
|
||||
// We don't use a resolver here, just return the value directly.
|
||||
/** @gqlType */
|
||||
/**
|
||||
* A URL that the client can use to upload a skin to S3, and then notify the server
|
||||
* when they're done.
|
||||
* @gqlType
|
||||
*/
|
||||
type UploadUrl = {
|
||||
/** @gqlField */
|
||||
id: string;
|
||||
|
|
@ -15,16 +19,34 @@ type UploadUrl = {
|
|||
md5: string;
|
||||
};
|
||||
|
||||
/** @gqlInput */
|
||||
/**
|
||||
* Input object used for a user to request an UploadUrl
|
||||
* @gqlInput
|
||||
*/
|
||||
type UploadUrlRequest = { filename: string; md5: string };
|
||||
|
||||
/** @gqlType UploadMutations */
|
||||
/**
|
||||
* Mutations for the upload flow
|
||||
*
|
||||
* 1. The user finds the md5 hash of their local files.
|
||||
* 2. (`get_upload_urls`) The user requests upload URLs for each of their files.
|
||||
* 3. The server returns upload URLs for each of their files which are not already in the collection.
|
||||
* 4. The user uploads each of their files to the URLs returned in step 3.
|
||||
* 5. (`report_skin_uploaded`) The user notifies the server that they're done uploading.
|
||||
* 6. (TODO) The user polls for the status of their uploads.
|
||||
*
|
||||
* @gqlType UploadMutations */
|
||||
class UploadMutationResolver {
|
||||
/** @gqlField */
|
||||
/**
|
||||
* Get a (possibly incompelte) list of UploadUrls for each of the files. If an
|
||||
* UploadUrl is not returned for a given hash, it means the file is already in
|
||||
* the collection.
|
||||
* @gqlField
|
||||
*/
|
||||
async get_upload_urls(
|
||||
{ files }: { files: UploadUrlRequest[] },
|
||||
{ ctx }
|
||||
): Promise<UploadUrl[]> {
|
||||
): Promise<Array<UploadUrl | null>> {
|
||||
const missing: UploadUrl[] = [];
|
||||
await Parallel.each(
|
||||
files,
|
||||
|
|
@ -41,7 +63,10 @@ class UploadMutationResolver {
|
|||
return missing;
|
||||
}
|
||||
|
||||
/** @gqlField */
|
||||
/**
|
||||
* Notify the server that the user is done uploading.
|
||||
* @gqlField
|
||||
*/
|
||||
async report_skin_uploaded(
|
||||
{ id, md5 }: { id: string; md5: string },
|
||||
req
|
||||
|
|
@ -64,15 +89,21 @@ function requireAuthed(handler) {
|
|||
};
|
||||
}
|
||||
|
||||
/** @gqlType Mutation */
|
||||
/**
|
||||
*
|
||||
* @gqlType Mutation */
|
||||
export default class MutationResolver {
|
||||
/** @gqlField */
|
||||
/**
|
||||
* Mutations for the upload flow
|
||||
* @gqlField */
|
||||
async upload(): Promise<UploadMutationResolver> {
|
||||
return new UploadMutationResolver();
|
||||
}
|
||||
/** @gqlField */
|
||||
/**
|
||||
* Send a message to the admin of the site. Currently this appears in Discord.
|
||||
* @gqlField */
|
||||
async send_feedback(
|
||||
{ message, email, url }: { message?: string; email?: string; url?: string },
|
||||
{ message, email, url }: { message: string; email?: string; url?: string },
|
||||
req
|
||||
): Promise<boolean> {
|
||||
req.notify({
|
||||
|
|
@ -84,7 +115,11 @@ export default class MutationResolver {
|
|||
return true;
|
||||
}
|
||||
|
||||
/** @gqlField */
|
||||
/**
|
||||
* Reject skin for tweeting
|
||||
*
|
||||
* **Note:** Requires being logged in
|
||||
* @gqlField */
|
||||
reject_skin(args: { md5: string }, req): Promise<boolean> {
|
||||
return this._reject_skin(args, req);
|
||||
}
|
||||
|
|
@ -100,7 +135,11 @@ export default class MutationResolver {
|
|||
return true;
|
||||
});
|
||||
|
||||
/** @gqlField */
|
||||
/**
|
||||
* Approve skin for tweeting
|
||||
*
|
||||
* **Note:** Requires being logged in
|
||||
* @gqlField */
|
||||
approve_skin(args: { md5: string }, req): Promise<boolean> {
|
||||
return this._approve_skin(args, req);
|
||||
}
|
||||
|
|
@ -116,7 +155,11 @@ export default class MutationResolver {
|
|||
return true;
|
||||
});
|
||||
|
||||
/** @gqlField */
|
||||
/**
|
||||
* Mark a skin as NSFW
|
||||
*
|
||||
* **Note:** Requires being logged in
|
||||
* @gqlField */
|
||||
mark_skin_nsfw(args: { md5: string }, req): Promise<boolean> {
|
||||
return this._mark_skin_nsfw(args, req);
|
||||
}
|
||||
|
|
@ -132,7 +175,11 @@ export default class MutationResolver {
|
|||
return true;
|
||||
});
|
||||
|
||||
/** @gqlField */
|
||||
/**
|
||||
* Request that an admin check if this skin is NSFW.
|
||||
* Unlike other review mutaiton endpoints, this one does not require being logged
|
||||
* in.
|
||||
* @gqlField */
|
||||
async request_nsfw_review_for_skin(
|
||||
{ md5 }: { md5: string },
|
||||
req
|
||||
|
|
|
|||
|
|
@ -21,8 +21,8 @@ export default class ReviewResolver {
|
|||
}
|
||||
|
||||
/**
|
||||
* The user who made the review (if known). **Note:** In the early days we
|
||||
* didn't track this, so many will be null.
|
||||
* The user who made the review (if known). **Note:** In the early days we didn't
|
||||
* track this, so many will be null.
|
||||
* @gqlField
|
||||
*/
|
||||
reviewer(): string {
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ const index = client.initIndex("Skins");
|
|||
class RootResolver extends MutationResolver {
|
||||
/**
|
||||
* Get a globally unique object by its ID.
|
||||
*
|
||||
* https://graphql.org/learn/global-object-identification/
|
||||
* @gqlField
|
||||
*/
|
||||
|
|
@ -84,6 +85,7 @@ class RootResolver extends MutationResolver {
|
|||
|
||||
/**
|
||||
* Get an archive.org item by its identifier. You can find this in the URL:
|
||||
*
|
||||
* https://archive.org/details/<identifier>/
|
||||
* @gqlField
|
||||
*/
|
||||
|
|
@ -100,6 +102,7 @@ class RootResolver extends MutationResolver {
|
|||
|
||||
/**
|
||||
* Fetch archive file by it's MD5 hash
|
||||
*
|
||||
* Get information about a file found within a skin's wsz/wal/zip archive.
|
||||
* @gqlField
|
||||
*/
|
||||
|
|
@ -127,7 +130,7 @@ class RootResolver extends MutationResolver {
|
|||
offset = 0,
|
||||
}: { query: string; first?: Int; offset?: Int },
|
||||
{ ctx }
|
||||
): Promise<ISkin[]> {
|
||||
): Promise<Array<ISkin | null>> {
|
||||
if (first > 1000) {
|
||||
throw new Error("Can only query 1000 records via search.");
|
||||
}
|
||||
|
|
@ -146,7 +149,11 @@ class RootResolver extends MutationResolver {
|
|||
);
|
||||
}
|
||||
|
||||
/** @gqlField */
|
||||
/**
|
||||
* All classic skins in the database
|
||||
*
|
||||
* **Note:** We don't currently support combining sorting and filtering.
|
||||
* @gqlField */
|
||||
skins({
|
||||
first = 10,
|
||||
offset = 0,
|
||||
|
|
@ -164,7 +171,9 @@ class RootResolver extends MutationResolver {
|
|||
return new SkinsConnection(first, offset, sort, filter);
|
||||
}
|
||||
|
||||
/** @gqlField */
|
||||
/**
|
||||
* All modern skins in the database
|
||||
* @gqlField */
|
||||
async modern_skins({
|
||||
first = 10,
|
||||
offset = 0,
|
||||
|
|
@ -178,7 +187,9 @@ class RootResolver extends MutationResolver {
|
|||
return new ModernSkinsConnection(first, offset);
|
||||
}
|
||||
|
||||
/** @gqlField */
|
||||
/**
|
||||
* A random skin that needs to be reviewed
|
||||
* @gqlField */
|
||||
async skin_to_review(_args: never, { ctx }): Promise<ISkin | null> {
|
||||
if (!ctx.authed()) {
|
||||
return null;
|
||||
|
|
@ -218,19 +229,22 @@ class RootResolver extends MutationResolver {
|
|||
/**
|
||||
* Get the status of a batch of uploads by md5s
|
||||
* @gqlField
|
||||
* @deprecated Prefer `upload_statuses` instead, were we operate on ids.
|
||||
*/
|
||||
async upload_statuses_by_md5(
|
||||
{ md5s }: { md5s: string[] },
|
||||
{ ctx }
|
||||
): Promise<SkinUpload[]> {
|
||||
): Promise<Array<SkinUpload | null>> {
|
||||
return this._upload_statuses({ keyName: "skin_md5", keys: md5s }, ctx);
|
||||
}
|
||||
|
||||
/** @gqlField */
|
||||
/**
|
||||
* Get the status of a batch of uploads by ids
|
||||
* @gqlField */
|
||||
async upload_statuses(
|
||||
{ ids }: { ids: string[] },
|
||||
{ ctx }
|
||||
): Promise<SkinUpload[]> {
|
||||
): Promise<Array<SkinUpload | null>> {
|
||||
return this._upload_statuses({ keyName: "id", keys: ids }, ctx);
|
||||
}
|
||||
|
||||
|
|
@ -256,27 +270,39 @@ class RootResolver extends MutationResolver {
|
|||
);
|
||||
}
|
||||
|
||||
/** @gqlField */
|
||||
/**
|
||||
* A namespace for statistics about the database
|
||||
* @gqlField */
|
||||
statistics(): DatabaseStatisticsResolver {
|
||||
return new DatabaseStatisticsResolver();
|
||||
}
|
||||
}
|
||||
|
||||
/** @gqlType */
|
||||
/**
|
||||
* Information about an attempt to upload a skin to the Museum.
|
||||
* @gqlType
|
||||
*/
|
||||
type SkinUpload = {
|
||||
/** @gqlField */
|
||||
id: string;
|
||||
/** @gqlField */
|
||||
status: SkinUploadStatus;
|
||||
/** @gqlField */
|
||||
/**
|
||||
* Skin that was uploaded. **Note:** This is null if the skin has not yet been
|
||||
* fully processed. (status == ARCHIVED)
|
||||
* @gqlField
|
||||
*/
|
||||
skin: ISkin | null;
|
||||
/** @gqlField */
|
||||
/**
|
||||
* Md5 hash given when requesting the upload URL.
|
||||
* @gqlField
|
||||
*/
|
||||
upload_md5: string;
|
||||
};
|
||||
|
||||
/** @gqlEnum */
|
||||
type SkinsSortOption =
|
||||
/*
|
||||
/**
|
||||
the Museum's (https://skins.webamp.org) special sorting rules.
|
||||
|
||||
Roughly speaking, it's:
|
||||
|
|
@ -314,6 +340,7 @@ Only the skins that have been tweeted
|
|||
|
||||
/**
|
||||
* The current status of a pending upload.
|
||||
*
|
||||
* **Note:** Expect more values here as we try to be more transparent about
|
||||
* the status of a pending uploads.
|
||||
* @gqlEnum
|
||||
|
|
|
|||
|
|
@ -3,26 +3,43 @@ import TweetModel from "../../../data/TweetModel";
|
|||
import { ISkin } from "./CommonSkinResolver";
|
||||
import SkinResolver from "./SkinResolver";
|
||||
|
||||
/** @gqlType Tweet */
|
||||
/**
|
||||
* A tweet made by @winampskins mentioning a Winamp skin
|
||||
* @gqlType Tweet
|
||||
*/
|
||||
export default class TweetResolver {
|
||||
_model: TweetModel;
|
||||
constructor(model: TweetModel) {
|
||||
this._model = model;
|
||||
}
|
||||
|
||||
/** @gqlField */
|
||||
/**
|
||||
* URL of the tweet. **Note:** Early on in the bot's life we just recorded
|
||||
* _which_ skins were tweeted, not any info about the actual tweet. This means we
|
||||
* don't always know the URL of the tweet.
|
||||
* @gqlField
|
||||
*/
|
||||
url(): string | null {
|
||||
return this._model.getUrl();
|
||||
}
|
||||
/** @gqlField */
|
||||
/**
|
||||
* Number of likes the tweet has received. Updated nightly. (Note: Recent likes on older tweets may not be reflected here)
|
||||
* @gqlField
|
||||
*/
|
||||
likes(): Int {
|
||||
return this._model.getLikes();
|
||||
}
|
||||
/** @gqlField */
|
||||
/**
|
||||
* Number of retweets the tweet has received. Updated nightly. (Note: Recent retweets on older tweets may not be reflected here)
|
||||
* @gqlField
|
||||
*/
|
||||
retweets(): Int {
|
||||
return this._model.getRetweets();
|
||||
}
|
||||
/** @gqlField */
|
||||
/**
|
||||
* The skin featured in this Tweet
|
||||
* @gqlField
|
||||
*/
|
||||
async skin(): Promise<ISkin | null> {
|
||||
const skin = await this._model.getSkin();
|
||||
if (skin == null) {
|
||||
|
|
|
|||
|
|
@ -449,40 +449,41 @@ type User {
|
|||
}
|
||||
|
||||
enum SkinsSortOption {
|
||||
"""
|
||||
the Museum's (https://skins.webamp.org) special sorting rules.
|
||||
# TODO: Add this back once Grats supports descriptions
|
||||
# """
|
||||
# the Museum's (https://skins.webamp.org) special sorting rules.
|
||||
|
||||
Roughly speaking, it's:
|
||||
# Roughly speaking, it's:
|
||||
|
||||
1. The four classic default skins
|
||||
2. Tweeted skins first (sorted by the number of likes/retweets)
|
||||
3. Approved, but not tweeted yet, skins
|
||||
4. Unreviwed skins
|
||||
5. Rejected skins
|
||||
6. NSFW skins
|
||||
"""
|
||||
# 1. The four classic default skins
|
||||
# 2. Tweeted skins first (sorted by the number of likes/retweets)
|
||||
# 3. Approved, but not tweeted yet, skins
|
||||
# 4. Unreviwed skins
|
||||
# 5. Rejected skins
|
||||
# 6. NSFW skins
|
||||
# """
|
||||
MUSEUM
|
||||
}
|
||||
|
||||
enum SkinsFilterOption {
|
||||
"""
|
||||
Only the skins that have been approved for tweeting
|
||||
"""
|
||||
# """
|
||||
# Only the skins that have been approved for tweeting
|
||||
# """
|
||||
APPROVED
|
||||
|
||||
"""
|
||||
Only the skins that have been rejected for tweeting
|
||||
"""
|
||||
# """
|
||||
# Only the skins that have been rejected for tweeting
|
||||
# """
|
||||
REJECTED
|
||||
|
||||
"""
|
||||
Only the skins that have been marked NSFW
|
||||
"""
|
||||
# """
|
||||
# Only the skins that have been marked NSFW
|
||||
# """
|
||||
NSFW
|
||||
|
||||
"""
|
||||
Only the skins that have been tweeted
|
||||
"""
|
||||
# """
|
||||
# Only the skins that have been tweeted
|
||||
# """
|
||||
TWEETED
|
||||
}
|
||||
|
||||
|
|
@ -493,32 +494,32 @@ The current status of a pending upload.
|
|||
the status of a pending uploads.
|
||||
"""
|
||||
enum SkinUploadStatus {
|
||||
"""
|
||||
The user has requested a URL, but the skin has not yet been processed.
|
||||
"""
|
||||
# """
|
||||
# The user has requested a URL, but the skin has not yet been processed.
|
||||
# """
|
||||
URL_REQUESTED
|
||||
|
||||
"""
|
||||
The user has notified us that the skin has been uploaded, but we haven't yet
|
||||
processed it.
|
||||
"""
|
||||
# """
|
||||
# The user has notified us that the skin has been uploaded, but we haven't yet
|
||||
# processed it.
|
||||
# """
|
||||
UPLOAD_REPORTED
|
||||
|
||||
"""
|
||||
An error occured processing the skin. Usually this is a transient error, and
|
||||
the skin will be retried at a later time.
|
||||
"""
|
||||
# """
|
||||
# An error occured processing the skin. Usually this is a transient error, and
|
||||
# the skin will be retried at a later time.
|
||||
# """
|
||||
ERRORED
|
||||
|
||||
"""
|
||||
An error occured processing the skin, but it was the fault of the server. It
|
||||
will be processed at a later date.
|
||||
"""
|
||||
# """
|
||||
# An error occured processing the skin, but it was the fault of the server. It
|
||||
# will be processed at a later date.
|
||||
# """
|
||||
DELAYED
|
||||
|
||||
"""
|
||||
The skin has been successfully added to the Museum.
|
||||
"""
|
||||
# """
|
||||
# The skin has been successfully added to the Museum.
|
||||
# """
|
||||
ARCHIVED
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,10 @@ export type TweetStatus =
|
|||
| "UNREVIEWED"
|
||||
| "NSFW";
|
||||
|
||||
/** @gqlEnum */
|
||||
/**
|
||||
* The judgement made about a skin by a moderator
|
||||
* @gqlEnum
|
||||
*/
|
||||
export type Rating = "APPROVED" | "REJECTED" | "NSFW";
|
||||
|
||||
export type SkinType = "MODERN" | "CLASSIC";
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue