Skip to content

Commit

Permalink
Refactor local json features
Browse files Browse the repository at this point in the history
  • Loading branch information
the1812 committed May 8, 2022
1 parent 1359589 commit 9b4fe4e
Show file tree
Hide file tree
Showing 12 changed files with 372 additions and 96 deletions.
152 changes: 151 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,14 +85,164 @@ thtag -s local-mp3
> 注意此数据源需要填入 MP3 的文件夹路径, 因此不能用于批量模式中
#### local-json
从 JSON 文件读取曲目信息, JSON 内数据的类型为 `Metadata[]` (定义位于`src/core/metadata.ts`)`[Metadata, ...Omit<Metadata, AlbumMetadata>[]]` (也就是除了第一个以外后续可以省略专辑字段). 可以使用此数据源处理一些原创专辑, 用法与 `local-mp3` 基本相同.
从 JSON 文件读取曲目信息, JSON 内数据的类型为 `Metadata[]` (定义位于`src/core/metadata.ts`). 可以使用此数据源处理一些原创专辑, 用法与 `local-mp3` 基本相同.

> 如果已存在名为 `metadata.json` 的文件, 程序会直接使用其中的曲目信息, 跳过曲目信息下载. 事先在各个文件夹中准备好 `metadata.json` 的话也是可以使用批量模式的.
```powershell
thtag -s local-json
```

为了方便使用, JSON 的内容可以进行一些省略:
- 整张专辑中相同的数据, 例如专辑名称 / 社团名称等, 只需要在第一首曲目中写上即可, 后续曲目均会使用相同的数据.
- 编曲者 (artists) 和作曲者 (composers) 相同时, 可以只写作曲者. (但不能只写编曲者, 因为东方同人曲的编曲者和作曲者一般是不同的, 作曲者从原曲信息就可以推断, 所以作曲者通常是省略的. 如果只写编曲者, 程序会让作曲者留空)
- 曲目编号可以省略, 程序将自动根据书写顺序进行标注.
- CD 编号 (多 CD 专辑) 只需要在每张 CD 的第一首标注出即可, 第一张 CD 可以省略不标.

以专辑 [Heart Essence](https://www.dizzylab.net/d/AOHCD-001/#/) 为例, 实体专辑是有 2 枚 CD 的 (dizzylab 上没有注明), 从第 11 首 `Initiate the Massacre` 起实际上是来自第 2 张 CD. 可以充分发挥上面的 4 个省略方法, 编写的 `metadata.json` 内容如下:

<details><summary><strong>metadata.json 示例</strong></summary>

```json
[
{
"title": "Broken Display",
"composers": [
"ASXX"
],
"album": "Heart Essence",
"albumOrder": "AOHCD-001",
"albumArtists": [
"Art of Heart"
],
"genres": [
"Hard Dance"
],
"year": "2021"
},
{
"title": "Alarm",
"composers": [
"Bincente Hole"
]
},
{
"title": "Coming Down",
"composers": [
"NceS"
]
},
{
"title": "All Night",
"composers": [
"Agoraphobia"
]
},
{
"title": "Faster Than Light",
"composers": [
"X-Eliminator"
]
},
{
"title": "Favorite Season",
"composers": [
"F.BING.KAI"
]
},
{
"title": "Dominator II",
"composers": [
"Danger Target"
]
},
{
"title": "Broke My Heart",
"composers": [
"NoisiestLunatic"
]
},
{
"title": "2 Hell",
"composers": [
"Ravenface86",
"Baneballcore"
]
},
{
"title": "Mud",
"composers": [
"_"
]
},
{
"title": "Initiate the Massacre",
"composers": [
"Joulez"
],
"discNumber": "2"
},
{
"title": "See The Light In You",
"composers": [
"Koregroo"
]
},
{
"title": "Quadrant",
"composers": [
"BlueWind"
]
},
{
"title": "H.T.G",
"composers": [
"Mrskey"
]
},
{
"title": "Make Your Body Shake",
"composers": [
"Greg Wu"
]
},
{
"title": "Madfcuk",
"composers": [
"Forkyrie"
]
},
{
"title": "Beats Indicating Tremendous Chordz & Harmonicz",
"composers": [
"Normal1zer"
]
},
{
"title": "Air Lock",
"composers": [
"潮音きつね_H",
"Rayven"
]
},
{
"title": "Deranged Deities of Forsaken",
"composers": [
"Nirotiy"
]
},
{
"title": "EuphoriA of My Heart",
"composers": [
"EuphoriA"
]
}
]
```

</details>


### 下载歌词
歌词相关的处理, 除了 `--lyric` 外的选项都会自动保存.

Expand Down
28 changes: 28 additions & 0 deletions dist/core/metadata/local-json/common-fields.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.commonFieldsPlugin = void 0;
/** 记录第一个 metadata 的公共字段, 后续可省略 */
const commonFieldsPlugin = () => {
let firstMetadata;
return ({ metadata, index }) => {
if (index === 0) {
firstMetadata = metadata;
}
if (index > 0) {
const albumDataFields = [
'album',
'albumOrder',
'albumArtists',
'genres',
'year',
'coverImage',
];
albumDataFields.forEach(field => {
if (!metadata[field]) {
metadata[field] = firstMetadata[field];
}
});
}
};
};
exports.commonFieldsPlugin = commonFieldsPlugin;
39 changes: 39 additions & 0 deletions dist/core/metadata/local-json/cover.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.fetchCoverPlugin = void 0;
const axios_1 = __importDefault(require("axios"));
/** 处理封面图片 */
const fetchCoverPlugin = ({ cover, config }) => {
let firstCoverBuffer = undefined;
const downloadRemoteCover = async (url) => {
return axios_1.default.get(url, {
responseType: 'arraybuffer',
timeout: config.timeout * 1000,
});
};
return async ({ metadata, index }) => {
if (index === 0) {
if (cover !== undefined) {
firstCoverBuffer = cover;
}
else if (typeof metadata.coverImage === 'string') {
const response = await downloadRemoteCover(metadata.coverImage);
firstCoverBuffer = response.data;
}
metadata.coverImage = firstCoverBuffer;
}
if (index > 0) {
if (metadata.coverImage === undefined && firstCoverBuffer !== undefined) {
metadata.coverImage = firstCoverBuffer;
}
else if (typeof metadata.coverImage === 'string') {
const response = await downloadRemoteCover(metadata.coverImage);
metadata.coverImage = response.data;
}
}
};
};
exports.fetchCoverPlugin = fetchCoverPlugin;
22 changes: 22 additions & 0 deletions dist/core/metadata/local-json/infer-number.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.inferNumberPlugin = void 0;
/** 自动推测 trackNumber 和 discNumber */
const inferNumberPlugin = () => {
let cachedTrackNumber = 1;
let cachedDiscNumber = 1;
return ({ metadata }) => {
if (metadata.discNumber && parseInt(metadata.discNumber) !== cachedDiscNumber) {
cachedDiscNumber = parseInt(metadata.discNumber);
cachedTrackNumber = 1;
}
if (!metadata.discNumber) {
metadata.discNumber = cachedDiscNumber.toString();
}
if (!metadata.trackNumber) {
metadata.trackNumber = cachedTrackNumber.toString();
}
cachedTrackNumber++;
};
};
exports.inferNumberPlugin = inferNumberPlugin;
58 changes: 10 additions & 48 deletions dist/core/metadata/local-json/local-json.js
Original file line number Diff line number Diff line change
@@ -1,63 +1,25 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.localJson = exports.LocalJson = void 0;
const metadata_source_1 = require("../metadata-source");
const fs_1 = require("fs");
const exists_1 = require("../../exists");
const axios_1 = __importDefault(require("axios"));
const infer_number_1 = require("./infer-number");
const common_fields_1 = require("./common-fields");
const cover_1 = require("./cover");
const omit_artists_1 = require("./omit-artists");
const plugins = [cover_1.fetchCoverPlugin, omit_artists_1.omitArtistsPlugin, infer_number_1.inferNumberPlugin, common_fields_1.commonFieldsPlugin];
class LocalJson extends metadata_source_1.MetadataSource {
/**
* Normalize JSON data from file:
* - Cover image buffer
* - Album metadata
*/
async normalize(metadatas, cover) {
if (!metadatas || metadatas.length === 0) {
return metadatas;
}
const [firstMetadata] = metadatas;
let cachedTrackNumber = 1;
let cachedDiscNumber = 1;
const pluginInstances = plugins.map(p => p({ cover, config: this.config }));
const results = await Promise.all(metadatas.map(async (metadata, index) => {
let coverBuffer = undefined;
if (cover !== undefined) {
coverBuffer = cover;
}
else if (typeof metadata.coverImage === 'string') {
const response = await axios_1.default.get(metadata.coverImage, {
responseType: 'arraybuffer',
timeout: this.config.timeout * 1000,
});
coverBuffer = response.data;
}
metadata.coverImage = coverBuffer;
if (metadata.discNumber && parseInt(metadata.discNumber) !== cachedDiscNumber) {
cachedDiscNumber = parseInt(metadata.discNumber);
cachedTrackNumber = 1;
}
if (!metadata.discNumber) {
metadata.discNumber = cachedDiscNumber.toString();
}
if (!metadata.trackNumber) {
metadata.trackNumber = cachedTrackNumber.toString();
}
cachedTrackNumber++;
if (index > 0) {
const albumDataFields = [
'album',
'albumOrder',
'albumArtists',
'genres',
'year',
'coverImage',
];
albumDataFields.forEach(field => {
if (!metadata[field]) {
metadata[field] = firstMetadata[field];
}
for (const instance of pluginInstances) {
await instance({
metadata,
index,
});
}
return metadata;
Expand Down
12 changes: 12 additions & 0 deletions dist/core/metadata/local-json/omit-artists.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.omitArtistsPlugin = void 0;
/** 省略 artists 且带有 composers 时, 使用 composers 填充 artists */
const omitArtistsPlugin = () => {
return ({ metadata }) => {
if (!metadata.artists && metadata.composers) {
metadata.artists = metadata.composers;
}
};
};
exports.omitArtistsPlugin = omitArtistsPlugin;
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "touhou-tagger",
"version": "1.3.0",
"version": "1.4.0",
"description": "从 THBWiki 自动填写东方 Project CD 曲目信息.",
"main": "dist/core/index.js",
"bin": {
Expand Down
27 changes: 27 additions & 0 deletions src/core/metadata/local-json/common-fields.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Metadata } from '../metadata'
import { LocalJsonPlugin } from './local-json'

/** 记录第一个 metadata 的公共字段, 后续可省略 */
export const commonFieldsPlugin: LocalJsonPlugin = () => {
let firstMetadata: Metadata
return ({ metadata, index }) => {
if (index === 0) {
firstMetadata = metadata
}
if (index > 0) {
const albumDataFields = [
'album',
'albumOrder',
'albumArtists',
'genres',
'year',
'coverImage',
]
albumDataFields.forEach(field => {
if (!metadata[field]) {
metadata[field] = firstMetadata[field]
}
})
}
}
}
Loading

0 comments on commit 9b4fe4e

Please sign in to comment.