Skip to content

Commit

Permalink
feat: add api for subject collect (#944)
Browse files Browse the repository at this point in the history
  • Loading branch information
everpcpc authored Feb 11, 2025
1 parent fa80b4d commit 652446e
Show file tree
Hide file tree
Showing 13 changed files with 800 additions and 55 deletions.
6 changes: 3 additions & 3 deletions drizzle/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -559,7 +559,7 @@ export const chiiSubjectImgs = mysqlTable('chii_subject_imgs', {
});

export const chiiSubjectInterests = mysqlTable('chii_subject_interests', {
id: int('interest_id').autoincrement().notNull(),
id: int('interest_id').autoincrement().notNull().primaryKey(),
uid: mediumint('interest_uid').notNull(),
subjectID: mediumint('interest_subject_id').notNull(),
subjectType: smallint('interest_subject_type').notNull(),
Expand Down Expand Up @@ -650,11 +650,11 @@ export const chiiSubjectPosts = mysqlTable('chii_subject_posts', {
});

export const chiiTagIndex = mysqlTable('chii_tag_neue_index', {
id: mediumint('tag_id').autoincrement().notNull(),
id: mediumint('tag_id').autoincrement().notNull().primaryKey(),
name: varchar('tag_name', { length: 30 }).notNull(),
cat: tinyint('tag_cat').notNull(),
type: tinyint('tag_type').notNull(),
totalCount: mediumint('tag_results').notNull(),
count: mediumint('tag_results').notNull(),
createdAt: int('tag_dateline').notNull(),
updatedAt: int('tag_lasttouch').notNull(),
});
Expand Down
3 changes: 2 additions & 1 deletion lib/subject/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,13 @@ import { RevType } from '@app/lib/orm/entity/index.ts';
import * as ormold from '@app/lib/orm/index.ts';
import { SubjectImageRepo, SubjectRepo } from '@app/lib/orm/index.ts';
import { extractDate } from '@app/lib/subject/date.ts';
import { TagCat } from '@app/lib/tag.ts';
import * as fetcher from '@app/lib/types/fetcher.ts';
import { DATE } from '@app/lib/utils/date.ts';
import { matchExpected } from '@app/lib/wiki.ts';
import { getSubjectPlatforms } from '@app/vendor';

import { SubjectType, TagCat } from './type.ts';
import { SubjectType } from './type.ts';

export const InvalidWikiSyntaxError = createError(
'INVALID_SYNTAX_ERROR',
Expand Down
9 changes: 2 additions & 7 deletions lib/subject/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ export enum EpisodeCollectionStatus {
Dropped = 3, // 抛弃
}

export interface UserEpisodeCollection {
id: number;
export interface UserEpisodeStatusItem {
eid: number;
type: EpisodeCollectionStatus;
}

Expand All @@ -74,11 +74,6 @@ export enum PersonType {
Person = 'prsn',
}

export enum TagCat {
Subject = 0,
Meta = 3,
}

export enum SubjectSort {
Rank = 'rank',
Trends = 'trends',
Expand Down
164 changes: 164 additions & 0 deletions lib/subject/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import * as php from '@trim21/php-serialize';
import { DateTime } from 'luxon';

import { db, decr, incr, op, type Txn } from '@app/drizzle/db.ts';
import type * as orm from '@app/drizzle/orm.ts';
import * as schema from '@app/drizzle/schema';

import type { CollectionType, UserEpisodeStatusItem } from './type';
import { EpisodeCollectionStatus, getCollectionTypeField } from './type';

/** 更新条目收藏计数,需要在事务中执行 */
export async function updateSubjectCollection(
t: Txn,
subjectID: number,
newType: CollectionType,
oldType?: CollectionType,
) {
if (oldType && oldType === newType) {
return;
}
const toUpdate: Record<string, op.SQL> = {};
toUpdate[getCollectionTypeField(newType)] = incr(
schema.chiiSubjects[getCollectionTypeField(newType)],
);
if (oldType) {
toUpdate[getCollectionTypeField(oldType)] = decr(
schema.chiiSubjects[getCollectionTypeField(oldType)],
);
}
await t
.update(schema.chiiSubjects)
.set(toUpdate)
.where(op.eq(schema.chiiSubjects.id, subjectID))
.limit(1);
}

function getRatingField(rate: number) {
return [
undefined,
'rate1',
'rate2',
'rate3',
'rate4',
'rate5',
'rate6',
'rate7',
'rate8',
'rate9',
'rate10',
][rate];
}

/** 更新条目评分,需要在事务中执行 */
export async function updateSubjectRating(
t: Txn,
subjectID: number,
oldRate: number,
newRate: number,
) {
if (oldRate === newRate) {
return;
}
const newField = getRatingField(newRate);
const oldField = getRatingField(oldRate);
const toUpdate: Record<string, op.SQL> = {};
if (newField) {
const field = newField as keyof orm.ISubjectFields;
toUpdate[field] = incr(schema.chiiSubjectFields[field]);
}
if (oldField) {
const field = oldField as keyof orm.ISubjectFields;
toUpdate[field] = decr(schema.chiiSubjectFields[field]);
}
if (Object.keys(toUpdate).length === 0) {
return;
}
await t
.update(schema.chiiSubjects)
.set(toUpdate)
.where(op.eq(schema.chiiSubjects.id, subjectID))
.limit(1);
}

/** 标记条目剧集为已观看,需要在事务中执行 */
export async function markEpisodesAsWatched(
t: Txn,
userID: number,
subjectID: number,
episodeIDs: number[],
revertOthers = false,
) {
const epStatusList: Record<number, UserEpisodeStatusItem> = {};
for (const episodeID of episodeIDs) {
epStatusList[episodeID] = {
eid: episodeID,
type: EpisodeCollectionStatus.Done,
};
}
const [current] = await t
.select()
.from(schema.chiiEpStatus)
.where(
op.and(op.eq(schema.chiiEpStatus.uid, userID), op.eq(schema.chiiEpStatus.sid, subjectID)),
);
if (current?.status) {
const oldList = parseSubjectEpStatus(current.status);
for (const x of Object.values(oldList)) {
if (episodeIDs.includes(x.eid)) {
continue;
}
if (revertOthers && x.type === EpisodeCollectionStatus.Done) {
epStatusList[x.eid] = {
eid: x.eid,
type: EpisodeCollectionStatus.None,
};
} else {
epStatusList[x.eid] = x;
}
}
const newStatus = php.stringify(epStatusList);
await t
.update(schema.chiiEpStatus)
.set({ status: newStatus, updatedAt: DateTime.now().toUnixInteger() })
.where(op.eq(schema.chiiEpStatus.id, current.id))
.limit(1);
} else {
const newStatus = php.stringify(epStatusList);
await t.insert(schema.chiiEpStatus).values({
uid: userID,
sid: subjectID,
status: newStatus,
updatedAt: DateTime.now().toUnixInteger(),
});
}
}

export function parseSubjectEpStatus(status: string): Record<number, UserEpisodeStatusItem> {
const result: Record<number, UserEpisodeStatusItem> = {};
if (!status) {
return result;
}
const epStatusList = php.parse(status) as Record<number, UserEpisodeStatusItem>;
for (const x of Object.values(epStatusList)) {
result[x.eid] = x;
}
return result;
}

export async function getEpStatus(
userID: number,
subjectID: number,
): Promise<Record<number, UserEpisodeStatusItem>> {
const [data] = await db
.select()
.from(schema.chiiEpStatus)
.where(
op.and(op.eq(schema.chiiEpStatus.uid, userID), op.eq(schema.chiiEpStatus.sid, subjectID)),
)
.limit(1);
if (!data) {
return {};
}
return parseSubjectEpStatus(data.status);
}
162 changes: 162 additions & 0 deletions lib/tag.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import { DateTime } from 'luxon';

import type { Txn } from '@app/drizzle/db.ts';
import { op } from '@app/drizzle/db.ts';
import * as schema from '@app/drizzle/schema.ts';
import { dam } from '@app/lib/dam';

export enum TagCat {
/** 条目, 对应的 type: 条目类型 */
Subject = 0,

/** 入口, 对应的 type: blog = 1 */
Entry = 1,

/** 同人, 对应的 type: doujin = 1 和 club = 2 */
Doujin = 2,

/** Wiki, 对应的 type: 条目类型 */
Meta = 3,
}

export function validateTags(tags: string[]): string[] {
const result = new Set<string>();
for (const tag of tags) {
const t = tag.trim().normalize('NFKC');
if (t.length < 2) {
continue;
}
if (dam.needReview(t)) {
continue;
}
result.add(t);
if (result.size >= 10) {
break;
}
}
return [...result].sort();
}

/**
* 插入用户收藏标签,需要在事务中执行
*
* @param t - 事务
* @param uid - 用户ID
* @param cat - 标签分类
* @param type - 标签类型
* @param mid - 条目 ID 等
* @param tags - 输入的标签
* @returns 清理后的标签
*/
export async function insertUserTags(
t: Txn,
uid: number,
cat: TagCat,
type: number,
mid: number,
tags: string[],
): Promise<string[]> {
tags = validateTags(tags);
await t
.delete(schema.chiiTagList)
.where(
op.and(
op.eq(schema.chiiTagList.userID, uid),
op.eq(schema.chiiTagList.cat, cat),
op.eq(schema.chiiTagList.type, type),
op.eq(schema.chiiTagList.mainID, mid),
),
);

const tagIDs = await ensureTags(t, cat, type, tags);
const tids = Object.values(tagIDs).sort();

if (tids.length > 0) {
const now = DateTime.now().toUnixInteger();
await t.insert(schema.chiiTagList).values(
tids.map((id) => ({
tagID: id,
userID: uid,
cat,
type,
mainID: mid,
createdAt: now,
})),
);
await updateTagResult(t, tids);
}
return tags;
}

export async function updateTagResult(t: Txn, tagIDs: number[]) {
const now = DateTime.now().toUnixInteger();
const counts = await t
.select({
tagID: schema.chiiTagList.tagID,
count: op.count(schema.chiiTagList.tagID),
})
.from(schema.chiiTagList)
.where(op.inArray(schema.chiiTagList.tagID, tagIDs))
.groupBy(schema.chiiTagList.tagID);
for (const item of counts) {
await t
.update(schema.chiiTagIndex)
.set({
count: item.count,
updatedAt: now,
})
.where(op.eq(schema.chiiTagIndex.id, item.tagID))
.limit(1);
}
}

export async function ensureTags(
t: Txn,
cat: TagCat,
type: number,
tags: string[],
): Promise<Record<string, number>> {
const tagIDs: Record<string, number> = {};
if (tags.length === 0) {
return tagIDs;
}

const existTags = await t
.select()
.from(schema.chiiTagIndex)
.where(
op.and(
op.eq(schema.chiiTagIndex.cat, cat),
op.eq(schema.chiiTagIndex.type, type),
op.inArray(schema.chiiTagIndex.name, tags),
),
);
for (const tag of existTags) {
tagIDs[tag.name] = tag.id;
}

const now = DateTime.now().toUnixInteger();
const insertTags = tags.filter((tag) => !tagIDs[tag]);
if (insertTags.length > 0) {
const insertResult = await t
.insert(schema.chiiTagIndex)
.values(
insertTags.map((tag) => ({
name: tag,
cat: cat,
type: type,
count: 0,
createdAt: now,
updatedAt: now,
})),
)
.$returningId();
for (const [idx, r] of insertResult.entries()) {
const tag = insertTags[idx];
if (tag) {
tagIDs[tag] = r.id;
}
}
}
return tagIDs;
}
Loading

0 comments on commit 652446e

Please sign in to comment.