Remove skinArchiveTools. It's been replaced by skin-database

This commit is contained in:
Jordan Eldredge 2021-01-01 00:32:09 -08:00
parent f555894de4
commit 7754730aec
13 changed files with 0 additions and 2662 deletions

View file

@ -1 +0,0 @@
assets/*

View file

@ -1,45 +0,0 @@
# Skin Archive Tools
**Note:** My goal is to merge this into the `skin-database` package.
A collection of scripts for managing the Internet Archive's collection of Winamp Skins.
It works as a data pipeline. The phases are:
## 1. Collect Skins
I collect skins into a single directory on my computer. There is no real organizational strategy within this directory. It's deeply nested and there are many duplicates, as it consists of multiple collections, small and large, from multiple sources.
We will also extrack skins for "packs" which are zip files containing multiple skins.
## 2. Skins Are Deduped
A script creates a single flat directory that contains each skin named only by its md5 hash.
## 3. Screenshots Are Taken
Yup.
## 4. Skins Are Synced to the Internet Archive
TODO
## Usage
```bash
yarn start
```
Note: This will probably break the first time it's run, since it requires a directory structure that is currently not checked in due to `.gitignore`.
Something like:
```
$ tree assets -d -L 1
assets
├── md5Invalids
├── md5Packs
├── md5Screenshots
├── md5Skins
└── skins
```

View file

@ -1,60 +0,0 @@
const childProcess = require("child_process");
const Bluebird = require("bluebird");
const Filehound = require("filehound");
function md5File(filePath) {
return new Promise((resolve, reject) => {
childProcess.execFile(`md5`, ["-q", filePath], (err, stdout) => {
if (err) {
// node couldn't execute the command
reject(err);
}
resolve(stdout.trimRight());
});
});
}
module.exports = async function collectSkins({ inputDir, cache }) {
console.log("Searching for files in", inputDir);
const paths = new Set();
Object.values(cache).forEach((skin) => {
skin.filePaths.forEach((filePath) => {
paths.add(filePath);
});
});
const files = await Filehound.create()
.ext(["zip", "wsz", "wal"])
.paths(inputDir)
.find();
console.log(`Found ${files.length} potential files`);
let i = 0;
const interval = setInterval(() => {
console.log(`Checked ${i} files...`);
}, 10000);
await Bluebird.map(
files,
async (filePath) => {
if (paths.has(filePath)) {
return;
}
i++;
const md5 = await md5File(filePath);
if (cache[md5]) {
const filePathsSet = new Set(cache[md5].filePaths);
filePathsSet.add(filePath);
cache[md5].filePaths = Array.from(filePathsSet);
} else {
cache[md5] = {
md5,
filePaths: [filePath],
};
}
},
{ concurrency: 10 }
);
clearInterval(interval);
return cache;
};

View file

@ -1,18 +0,0 @@
const exec = require("child_process").exec;
const shellescape = require("shell-escape");
const getColor = (imgPath) => {
return new Promise((resolve, reject) => {
const excapedImgPath = shellescape([imgPath]);
const command = `convert ${excapedImgPath} -scale 1x1\! -format '%[pixel:u]' info:-`;
exec(command, (error, stdout) => {
if (error !== null) {
reject(error);
return;
}
resolve(stdout.slice(1));
});
});
};
module.exports = { getColor };

View file

@ -1,8 +0,0 @@
const FILE_TYPES = {
INVALID: "INVALID",
CLASSIC: "CLASSIC",
MODERN: "MODERN",
PACK: "PACK",
};
module.exports = { FILE_TYPES };

View file

@ -1,385 +0,0 @@
const path = require("path");
const fs = require("fs");
const Bluebird = require("bluebird");
const collectSkins = require("./collectSkins");
const Utils = require("./utils");
const { FILE_TYPES } = require("./constants");
const { getColor } = require("./color");
const { extractTextData } = require("./readme");
const Shooter = require("./shooter");
/**
* Usage
*
* command <input_dir> <output_dir> <screenshot_dir>
*/
const CACHE_PATH = "/Volumes/Mobile Backup/skins/cache/";
const [
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_node,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_thisFile,
rawInputDir,
rawOutputDir,
] = process.argv;
const inputDir = path.resolve(rawInputDir);
const outputDir = path.resolve(rawOutputDir);
// It's nice to be able to fiddle these manually
const collect = false;
const getTypes = false;
const moveHome = false;
const screenshots = false;
const detectGenerated = false;
const archive = false;
const force = false;
const color = false;
const favorites = false;
const readme = true;
const cacheFilePath = path.join(CACHE_PATH, "info.json");
function getPath(skin) {
const filePath = skin.filePaths[0];
if (filePath == null) {
throw new Error(`Missing file path for ${skin.md5}`);
}
return filePath;
}
async function main() {
// Run this if new files are added
const start = Date.now();
let cache = JSON.parse(fs.readFileSync(cacheFilePath, "utf8"));
console.log("parsed cache in ", (Date.now() - start) / 1000, "seconds");
// Writing a backup
const cacheBackupFilePath = path.join(
CACHE_PATH,
`backup-${Date.now()}.json`
);
fs.writeFileSync(cacheBackupFilePath, JSON.stringify(cache, null, 2));
console.log(`Wrote a backup of the cache to ${cacheBackupFilePath}`);
function report() {
let classic = 0;
let modern = 0;
let invalid = 0;
let screenshotCount = 0;
let archived = 0;
let generated = 0;
Object.values(cache).forEach((skin) => {
switch (skin.type) {
case FILE_TYPES.CLASSIC:
classic++;
break;
case FILE_TYPES.MODERN:
modern++;
break;
case FILE_TYPES.INVALID:
invalid++;
break;
}
if (skin.screenshotPath) {
screenshotCount++;
}
if (skin.iaItemName) {
archived++;
}
if (skin.generated === true) {
generated++;
}
});
console.log(`${classic} classic skins`);
console.log(`${modern} modern skins`);
console.log(`${invalid} invalid skins`);
console.log(`${screenshotCount} screenshots`);
console.log(`${generated} generated`);
console.log(`${archived} archived`);
}
function save() {
fs.writeFileSync(cacheFilePath, JSON.stringify(cache, null, 2));
}
console.log("Starting with...");
report();
if (collect) {
console.log("Collecting skins");
cache = await collectSkins({ inputDir, cache });
save();
}
if (moveHome) {
console.log("Moving skins to md5AllSkins");
Object.values(cache).forEach((skin) => {
const md5Path = path.join(outputDir, "md5AllSkins", `${skin.md5}`);
if (md5Path === skin.md5Path) {
return;
}
if (!fs.existsSync(md5Path)) {
fs.linkSync(getPath(skin), md5Path);
}
skin.md5Path = md5Path;
});
save();
console.log("Moving classic skins to md5Skins");
Object.values(cache)
.filter((skin) => skin.type === FILE_TYPES.CLASSIC)
.forEach((skin) => {
const classicMd5Path = path.join(
outputDir,
"md5Skins",
`${skin.md5}.wsz`
);
if (classicMd5Path === skin.classicMd5Path) {
return;
}
if (!fs.existsSync(classicMd5Path)) {
fs.linkSync(getPath(skin), classicMd5Path);
}
skin.classicMd5Path = classicMd5Path;
});
save();
}
if (getTypes) {
console.log("Computing skin types");
const skins = Object.values(cache);
let i = 0;
console.log(`Ran on ${i} of ${skins.length} skins`);
const interval = setInterval(() => {
console.log(`Ran on ${i} of ${skins.length} skins`);
console.log("Saving...");
save();
}, 10000);
await Bluebird.map(
skins,
async (skin) => {
if (skin.type && force === false) {
return;
}
skin.type = await Utils.skinType(getPath(skin));
i++;
},
{ concurrency: 10 }
);
clearInterval(interval);
save();
}
if (screenshots) {
console.log("Taking screenshots");
const skins = Object.values(cache);
let i = 0;
let j = 0;
const interval = setInterval(() => {
console.log(`Took ${i} screenshots`);
console.log(`Found ${j} screenshots`);
console.log("Saving...");
save();
}, 10000);
const needScreenshot = skins.filter((skin) => {
return skin.type === FILE_TYPES.CLASSIC && !skin.screenshotPath;
});
console.log(`Need to take ${needScreenshot.length} screenshots...`);
if (needScreenshot.length > 0) {
const shooter = new Shooter();
await shooter.init();
for (const skin of needScreenshot) {
const screenshotPath = path.join(
outputDir,
"md5Screenshots",
`${skin.md5}.png`
);
if (!fs.existsSync(screenshotPath) || force) {
await shooter.takeScreenshot(getPath(skin), screenshotPath, {
minify: true,
});
i++;
} else {
j++;
}
skin.screenshotPath = screenshotPath;
}
shooter.dispose();
}
clearInterval(interval);
console.log(`Took ${i} screenshots`);
console.log(`Found ${j} screenshots`);
save();
}
if (detectGenerated) {
const unknownSkins = Object.values(cache).filter(
(skin) => skin.type === FILE_TYPES.CLASSIC && skin.generated == null
);
console.log(
`Found ${unknownSkins.length} skins to check if they are generated`
);
let i = 0;
let j = 0;
const interval = setInterval(() => {
console.log(`Found ${i} generated skins out of ${j}`);
console.log("Saving...");
save();
}, 10000);
await Bluebird.map(
unknownSkins,
async (skin) => {
const generated = await Utils.skinWasGenerated(skin.classicMd5Path);
j++;
if (generated) {
i++;
}
skin.generated = generated;
},
{ concurrency: 10 }
);
clearInterval(interval);
save();
}
if (archive) {
const classicSkins = Object.values(cache).filter(
(skin) => skin.type === FILE_TYPES.CLASSIC && skin.iaItemName == null
);
const seenItemNames = new Set();
Object.values(cache).forEach((skin) => {
if (skin.iaItemName != null) {
seenItemNames.add(skin.iaItemName.toLowerCase());
}
});
await Bluebird.map(
classicSkins,
async (skin) => {
const itemName = Utils.getArchiveItemName(skin);
console.log("going to upload ", itemName);
if (seenItemNames.has(itemName.toLowerCase())) {
console.log(itemName, "already exists");
return;
}
try {
await Utils.archive(skin, itemName);
} catch (e) {
console.error(e);
return;
}
console.log("Uploaded:", itemName);
skin.iaItemName = itemName;
seenItemNames.add(itemName.toLowerCase());
save();
},
{ concurrency: 4 }
);
save();
}
if (color) {
const classicSkins = Object.values(cache).filter(
(skin) => skin.type === FILE_TYPES.CLASSIC
);
let i = 0;
const interval = setInterval(() => {
console.log(`Found the average color of ${i} skins`);
console.log("Saving...");
save();
}, 10000);
await Bluebird.map(
classicSkins,
async (skin) => {
if (skin.screenshotPath == null) {
console.log("Could not extract color from skin");
return;
}
if (skin.averageColor != null && !force) {
return;
}
const averageColor = await getColor(skin.screenshotPath);
skin.averageColor = averageColor;
i++;
},
{ concurrency: 10 }
);
clearInterval(interval);
save();
}
if (favorites) {
const content = fs.readFileSync(
path.join("/Volumes/Mobile Backup/skins/", "likes.txt"),
"utf8"
);
content.split("\n").forEach((line) => {
if (!line.length) {
return;
}
const [hash, likes] = line.split(" ");
const skin = cache[hash];
if (!skin) {
console.log(`Found like for unknown skin ${hash}`);
return;
}
skin.twitterLikes = likes;
});
save();
}
if (readme) {
const classicSkins = Object.values(cache).filter(
(skin) => skin.type === FILE_TYPES.CLASSIC
);
let i = 0;
const interval = setInterval(() => {
console.log(`Found readme text for ${i} skins`);
console.log("Saving...");
save();
}, 10000);
await Bluebird.map(
classicSkins,
async (skin) => {
const readmePath = path.join(outputDir, "readmes", `${skin.md5}.txt`);
// if (skin.readmePath || skin.emails) {
if (skin.readmePath !== undefined) {
return;
}
if (fs.existsSync(readmePath)) {
skin.readmePath = readmePath;
i++;
return;
}
const { emails, raw } = await extractTextData(skin.md5Path);
skin.emails = emails;
if (raw) {
fs.writeFileSync(readmePath, raw);
skin.readmePath = readmePath;
} else {
// Note that we extracted it, but found nothing
skin.readmePath = null;
i++;
}
},
{ concurrency: 10 }
);
clearInterval(interval);
save();
}
console.log("Ended with...");
report();
save();
}
// Blastoff!
main();

View file

@ -1,46 +0,0 @@
const { exec } = require("child_process");
const reg = /[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/gi;
const extract = (value) => (value && value.match(reg)) || [];
async function extractTextData(skinPath) {
const ignoreFiles = [
"genex.txt",
"genexinfo.txt",
"gen_gslyrics.txt",
"region.txt",
"pledit.txt",
"viscolor.txt",
"winampmb.txt",
"gen_ex help.txt",
"mbinner.txt",
// Skinning Updates.txt ?
];
const ignoreArgs = ignoreFiles
.map((file) => `-x "**/${file}" ${file}`)
.join(" ");
// Change -p to -c to get context of which file is missing
const debug = false;
const listFlag = debug ? "-c" : "-p";
// TODO: Escape path
const cmd = `unzip ${listFlag} -C "${skinPath}" "file_id.diz" "*.txt" ${ignoreArgs}`;
const raw = await new Promise((resolve) => {
exec(cmd, (error, stdout) => {
if (error != null) {
// reject(error);
// return;
}
resolve(stdout);
});
});
return { raw, emails: extract(raw) };
}
module.exports = {
extractTextData,
};

View file

@ -1,87 +0,0 @@
const path = require("path");
const puppeteer = require("puppeteer");
const imagemin = require("imagemin");
const imageminOptipng = require("imagemin-optipng");
function min(imgPath) {
return imagemin([imgPath], path.dirname(imgPath), {
use: [imageminOptipng()],
});
}
class Shooter {
constructor() {
this._initialized = false;
}
async init() {
this._browser = await puppeteer.launch();
this._page = await this._browser.newPage();
this._page.setViewport({ width: 275, height: 116 * 3 });
this._page.on("console", (...args) => {
console.log("page log:", ...args);
});
this._page.on("error", (e) => {
console.log(`Page error: ${e.toString()}`);
});
this._page.on("dialog", async (dialog) => {
console.log(`Page dialog ${dialog.message()}`);
await dialog.dismiss();
});
const url = `http://localhost:8080/?screenshot=1`;
await this._page.goto(url);
await this._page.waitForSelector("#main-window", { timeout: 2000 });
await this._page.evaluate(() => {
// Needed to allow for transparent screenshots
window.document.body.style.background = "none";
});
this._initialized = true;
}
async _ensureInitialized() {
if (!this._initialized) {
await this.init();
}
}
async takeScreenshot(skin, screenshotPath, { minify = false }) {
await this._ensureInitialized();
console.log("Going to try", screenshotPath, skin);
try {
console.log("geting input");
const handle = await this._page.$("#webamp-file-input");
console.log("uploading skin");
await handle.uploadFile(skin);
console.log("waiting for skin to load...");
await this._page.evaluate(() => {
return window.__webamp.skinIsLoaded();
});
console.log("waiting for screenshot");
await this._page.screenshot({
path: screenshotPath,
omitBackground: true, // Make screenshot transparent
// https://github.com/GoogleChrome/puppeteer/issues/703#issuecomment-366041479
clip: { x: 0, y: 0, width: 275, height: 116 * 3 },
});
console.log("Wrote screenshot to", screenshotPath);
if (minify) {
min(screenshotPath);
}
console.log("Minified", screenshotPath);
} catch (e) {
console.error("Something went wrong, restarting browser", e);
await this.dispose();
await this.init();
throw e;
}
}
async dispose() {
this._ensureInitialized();
await this._page.close();
await this._browser.close();
this._initialized = false;
}
}
module.exports = Shooter;

View file

@ -1,36 +0,0 @@
const path = require("path");
const fs = require("fs");
const fsPromises = require("fs").promises;
const Shooter = require("./shooter");
module.exports = async function takeScreenshots(skinDir, screenshotDir) {
console.log(`Looking for skins in ${skinDir}`);
const files = await fsPromises.readdir(skinDir);
console.log(`Found ${files.length} skins`);
let skipCount = 0;
let shotCount = 0;
const interval = setInterval(() => {
console.log(`Update: Skipped: ${skipCount}`);
console.log(`Update: Shot: ${shotCount}`);
}, 10000);
const shooter = new Shooter();
for (const fileName of files) {
const filePath = path.join(skinDir, fileName);
if (!fs.existsSync(filePath)) {
throw new Error("wat", filePath);
}
const md5 = path.basename(filePath, ".wsz");
const screenshotPath = path.join(screenshotDir, `${md5}.png`);
if (fs.existsSync(screenshotPath)) {
skipCount++;
continue;
}
await shooter.takeScreenshot(filePath, screenshotPath, {
minify: true,
});
shotCount++;
}
clearInterval(interval);
console.log(`FINAL: Skipped: ${skipCount}`);
console.log(`FINAL: Shot: ${shotCount}`);
};

View file

@ -1,121 +0,0 @@
const fsPromises = require("fs").promises;
const path = require("path");
const childProcess = require("child_process");
const JSZip = require("jszip");
const { FILE_TYPES } = require("./constants");
// Reduce an array down to it's unique value given an async hasher function
async function unique(arr, hasher) {
const seen = new Set();
const items = [];
await Promise.all(
arr.map(async (item) => {
const key = await hasher(item);
if (seen.has(key)) {
return;
}
seen.add(key);
items.push(item);
})
);
return items;
}
function md5File(filePath) {
return new Promise((resolve, reject) => {
childProcess.execFile(`md5`, ["-q", filePath], (err, stdout) => {
if (err) {
// node couldn't execute the command
reject(err);
}
resolve(stdout.trimRight());
});
});
}
function skinWasGenerated(filePath) {
return new Promise((resolve) => {
childProcess.execFile(
`zipgrep`,
["This file was created by Skinamp", filePath],
(err) => {
if (err) {
// node couldn't execute the command
resolve(false);
}
resolve(true);
}
);
});
}
function getArchiveItemName(skin) {
const { filePaths } = skin;
const filename = `${path.parse(filePaths[0]).name}`;
const token = filename.replace(/[^0-9a-zA-Z]+/g, "_");
return `winampskin_${token}`;
}
function getArchiveTitle(skin) {
const { filePaths } = skin;
const filename = `${path.parse(filePaths[0]).name}`;
return `Winamp Skin: ${filename}`;
}
function archive(skin, itemName) {
return new Promise((resolve, reject) => {
const { classicMd5Path, filePaths, screenshotPath } = skin;
const filename = `${path.parse(filePaths[0]).name}`;
childProcess.execFile(
`python`,
[
"/Users/jordaneldredge/projects/ia-skins/sync.py",
classicMd5Path,
screenshotPath,
`${filename}.wsz`,
getArchiveTitle(skin),
itemName,
],
(err, stdout) => {
if (err) {
// node couldn't execute the command
reject(err);
}
resolve(stdout.trimRight());
}
);
});
}
async function skinType(skinPath) {
if (skinPath.endsWith(".wsz")) {
return FILE_TYPES.CLASSIC;
} else if (skinPath.endsWith(".wal")) {
return FILE_TYPES.MODERN;
}
const buffer = await fsPromises.readFile(skinPath);
try {
const zip = await JSZip.loadAsync(buffer);
if (zip.file(/.*\.(wsz|wal|zip)$/i).length) {
// This is a collection of skins
return FILE_TYPES.PACK;
}
const mains = zip.file(/^(.+\/)main\.bmp$/i);
return mains.length === 1 ? FILE_TYPES.CLASSIC : FILE_TYPES.MODERN;
} catch (e) {
return FILE_TYPES.INVALID;
}
}
module.exports = {
unique,
skinType,
skinWasGenerated,
md5File,
archive,
getArchiveItemName,
getArchiveTitle,
};

View file

@ -1,58 +0,0 @@
const path = require("path");
const fs = require("fs");
const fsPromises = require("fs").promises;
const fetch = require("node-fetch");
async function main() {
const screenshotDir = "/Volumes/Mobile Backup/skins/md5Screenshots/";
const skinDir = "/Volumes/Mobile Backup/skins/md5Skins/";
const filenamesPath = "/Volumes/Mobile Backup/skins/filenames.txt";
const filenames = await fsPromises.readFile(filenamesPath, "utf8");
const filenamesMap = new Map();
filenames.split("\n").forEach((line) => {
const md5 = line.slice(0, 32);
const filename = line.slice(33);
filenamesMap.set(md5, filename);
});
const skins = await fsPromises.readdir(skinDir);
const screenshots = await fsPromises.readdir(screenshotDir);
const skinMd5s = skins.map((skinPath) => path.basename(skinPath, ".wsz"));
const screenshotMd5s = screenshots.map((screenshotPath) =>
path.basename(screenshotPath, ".png")
);
const skinMd5sSet = new Set(skinMd5s);
const missing = screenshotMd5s.filter((md5) => !skinMd5sSet.has(md5));
await Promise.all(
missing.map(async (md5) => {
const filename = filenamesMap.get(md5);
if (filename == null) {
return;
}
const dest = path.join(
"/Volumes/Mobile Backup/skins/skins/recovered/",
`${filename}`
);
if (fs.existsSync(dest)) {
return;
}
const buffer = await fetch(
`https://s3.amazonaws.com/webamp-uploaded-skins/skins/${md5}.wsz`
);
await fsPromises.writeFile(dest, buffer);
console.log(dest);
})
);
// console.log(missingFiles);
}
main();

View file

@ -1,28 +0,0 @@
{
"name": "skinarchivetools",
"version": "1.0.0",
"description": "A collection of scripts for managing the Internet Archive's collection of Winamp Skins.",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node lib/index.js \"/Volumes/Mobile Backup/skins/skins/\" \"/Volumes/Mobile Backup/skins/\" \"/Volumes/Mobile Backup/skins/pathnames.txt\"",
"sync-skins": "aws s3 sync --acl=public-read \"/Volumes/Mobile Backup/skins/md5Skins/\" s3://webamp-uploaded-skins/skins/",
"sync-screenshots": "aws s3 sync --acl=public-read \"/Volumes/Mobile Backup/skins/md5Screenshots/\" s3://webamp-uploaded-skins/screenshots/",
"sync": "npm run sync-skins && npm run sync-screenshots",
"clean": "cd md5Skins && find . -delete"
},
"author": "",
"license": "ISC",
"dependencies": {
"bluebird": "^3.5.3",
"filehound": "^1.17.0",
"imagemin": "^7.0.0",
"imagemin-optipng": "^6.0.0",
"jszip": "^3.1.5",
"lodash": "^4.17.13",
"md5-file": "^4.0.0",
"node-fetch": "^2.3.0",
"puppeteer": "^1.12.2",
"shell-escape": "^0.2.0"
}
}

File diff suppressed because it is too large Load diff