refactor: refactor subscriptions

This commit is contained in:
2025-06-03 02:21:49 +08:00
parent 5645645c5f
commit a3fd03d32a
30 changed files with 2612 additions and 2034 deletions

View File

@@ -0,0 +1,16 @@
import type { Provider } from '@outposts/injection-js';
import { MikanService } from './services/mikan.service';
import { SubscriptionService } from './services/subscription.service';
export function provideRecorder(): Provider[] {
return [
{
provide: MikanService,
useClass: MikanService,
},
{
provide: SubscriptionService,
useClass: SubscriptionService,
},
];
}

View File

@@ -0,0 +1,192 @@
import { UnimplementedError } from '@/infra/errors/common';
import { SubscriptionCategoryEnum } from '@/infra/graphql/gql/graphql';
import { type ArkErrors, type } from 'arktype';
export const MIKAN_UNKNOWN_FANSUB_NAME = '生肉/不明字幕';
export const MIKAN_UNKNOWN_FANSUB_ID = '202';
export const MIKAN_ACCOUNT_MANAGE_PAGE_PATH = '/Account/Manage';
export const MIKAN_SEASON_FLOW_PAGE_PATH = '/Home/BangumiCoverFlow';
export const MIKAN_BANGUMI_HOMEPAGE_PATH = '/Home/Bangumi';
export const MIKAN_BANGUMI_EXPAND_SUBSCRIBED_PAGE_PATH = '/Home/ExpandBangumi';
export const MIKAN_EPISODE_HOMEPAGE_PATH = '/Home/Episode';
export const MIKAN_BANGUMI_POSTER_PATH = '/images/Bangumi';
export const MIKAN_EPISODE_TORRENT_PATH = '/Download';
export const MIKAN_SUBSCRIBER_SUBSCRIPTION_RSS_PATH = '/RSS/MyBangumi';
export const MIKAN_BANGUMI_RSS_PATH = '/RSS/Bangumi';
export const MIKAN_BANGUMI_ID_QUERY_KEY = 'bangumiId';
export const MIKAN_FANSUB_ID_QUERY_KEY = 'subgroupid';
export const MIKAN_SUBSCRIBER_SUBSCRIPTION_TOKEN_QUERY_KEY = 'token';
export const MIKAN_SEASON_STR_QUERY_KEY = 'seasonStr';
export const MIKAN_YEAR_QUERY_KEY = 'year';
export const MikanSubscriptionCategoryEnum = {
MikanBangumi: SubscriptionCategoryEnum.MikanBangumi,
MikanSeason: SubscriptionCategoryEnum.MikanSeason,
MikanSubscriber: SubscriptionCategoryEnum.MikanSubscriber,
} as const;
export type MikanSubscriptionCategoryEnum =
(typeof MikanSubscriptionCategoryEnum)[keyof typeof MikanSubscriptionCategoryEnum];
export const MikanSeasonEnum = {
Spring: '春',
Summer: '夏',
Autumn: '秋',
Winter: '冬',
} as const;
export type MikanSeasonEnum =
(typeof MikanSeasonEnum)[keyof typeof MikanSeasonEnum];
export const MikanSeasonSchema = type.enumerated(
MikanSeasonEnum.Spring,
MikanSeasonEnum.Summer,
MikanSeasonEnum.Autumn,
MikanSeasonEnum.Winter
);
export const MikanSubscriptionBangumiSourceUrlSchema = type({
category: `'${SubscriptionCategoryEnum.MikanBangumi}'`,
mikanBangumiId: 'string>0',
mikanFansubId: 'string>0',
});
export type MikanSubscriptionBangumiSourceUrl =
typeof MikanSubscriptionBangumiSourceUrlSchema.infer;
export const MikanSubscriptionSeasonSourceUrlSchema = type({
category: `'${SubscriptionCategoryEnum.MikanSeason}'`,
seasonStr: MikanSeasonSchema,
year: 'number>0',
});
export type MikanSubscriptionSeasonSourceUrl =
typeof MikanSubscriptionSeasonSourceUrlSchema.infer;
export const MikanSubscriptionSubscriberSourceUrlSchema = type({
category: `'${SubscriptionCategoryEnum.MikanSubscriber}'`,
mikanSubscriptionToken: 'string>0',
});
export type MikanSubscriptionSubscriberSourceUrl =
typeof MikanSubscriptionSubscriberSourceUrlSchema.infer;
export const MikanSubscriptionSourceUrlSchema =
MikanSubscriptionBangumiSourceUrlSchema.or(
MikanSubscriptionSeasonSourceUrlSchema
).or(MikanSubscriptionSubscriberSourceUrlSchema);
export type MikanSubscriptionSourceUrl =
typeof MikanSubscriptionSourceUrlSchema.infer;
export function isSubscriptionMikanCategory(
category: SubscriptionCategoryEnum
): category is MikanSubscriptionCategoryEnum {
return (
category === SubscriptionCategoryEnum.MikanBangumi ||
category === SubscriptionCategoryEnum.MikanSeason ||
category === SubscriptionCategoryEnum.MikanSubscriber
);
}
export function buildMikanSubscriptionSeasonSourceUrl(
mikanBaseUrl: string,
formParts: MikanSubscriptionSeasonSourceUrl
): URL {
const u = new URL(mikanBaseUrl);
u.pathname = MIKAN_SEASON_FLOW_PAGE_PATH;
u.searchParams.set(MIKAN_YEAR_QUERY_KEY, formParts.year.toString());
u.searchParams.set(MIKAN_SEASON_STR_QUERY_KEY, formParts.seasonStr);
return u;
}
export function buildMikanSubscriptionBangumiSourceUrl(
mikanBaseUrl: string,
formParts: MikanSubscriptionBangumiSourceUrl
): URL {
const u = new URL(mikanBaseUrl);
u.pathname = MIKAN_BANGUMI_RSS_PATH;
u.searchParams.set(MIKAN_BANGUMI_ID_QUERY_KEY, formParts.mikanBangumiId);
u.searchParams.set(MIKAN_FANSUB_ID_QUERY_KEY, formParts.mikanFansubId);
return u;
}
export function buildMikanSubscriptionSubscriberSourceUrl(
mikanBaseUrl: string,
formParts: MikanSubscriptionSubscriberSourceUrl
): URL {
const u = new URL(mikanBaseUrl);
u.pathname = MIKAN_SUBSCRIBER_SUBSCRIPTION_RSS_PATH;
u.searchParams.set(
MIKAN_SUBSCRIBER_SUBSCRIPTION_TOKEN_QUERY_KEY,
formParts.mikanSubscriptionToken
);
return u;
}
export function buildMikanSubscriptionSourceUrl(
mikanBaseUrl: string,
formParts: MikanSubscriptionSourceUrl
): URL {
if (formParts.category === SubscriptionCategoryEnum.MikanBangumi) {
return buildMikanSubscriptionBangumiSourceUrl(mikanBaseUrl, formParts);
}
if (formParts.category === SubscriptionCategoryEnum.MikanSeason) {
return buildMikanSubscriptionSeasonSourceUrl(mikanBaseUrl, formParts);
}
if (formParts.category === SubscriptionCategoryEnum.MikanSubscriber) {
return buildMikanSubscriptionSubscriberSourceUrl(mikanBaseUrl, formParts);
}
throw new UnimplementedError(
// @ts-ignore
`source url category = ${formParts.category as any} is not implemented`
);
}
export function extractMikanSubscriptionSeasonSourceUrl(
sourceUrl: string
): MikanSubscriptionSeasonSourceUrl | ArkErrors {
const u = new URL(sourceUrl);
return MikanSubscriptionSeasonSourceUrlSchema({
category: SubscriptionCategoryEnum.MikanSeason,
seasonStr: u.searchParams.get(
MIKAN_SEASON_STR_QUERY_KEY
) as MikanSeasonEnum,
year: Number(u.searchParams.get(MIKAN_YEAR_QUERY_KEY)),
});
}
export function extractMikanSubscriptionBangumiSourceUrl(
sourceUrl: string
): MikanSubscriptionBangumiSourceUrl | ArkErrors {
const u = new URL(sourceUrl);
return MikanSubscriptionBangumiSourceUrlSchema({
category: SubscriptionCategoryEnum.MikanBangumi,
mikanBangumiId: u.searchParams.get(MIKAN_BANGUMI_ID_QUERY_KEY),
mikanFansubId: u.searchParams.get(MIKAN_FANSUB_ID_QUERY_KEY),
});
}
export function extractMikanSubscriptionSubscriberSourceUrl(
sourceUrl: string
): MikanSubscriptionSubscriberSourceUrl | ArkErrors {
const u = new URL(sourceUrl);
return MikanSubscriptionSubscriberSourceUrlSchema({
category: SubscriptionCategoryEnum.MikanSubscriber,
mikanSubscriptionToken: u.searchParams.get(
MIKAN_SUBSCRIBER_SUBSCRIPTION_TOKEN_QUERY_KEY
),
});
}
export function extractMikanSubscriptionSourceUrl(
sourceUrl: string
): MikanSubscriptionSourceUrl | ArkErrors {
const u = new URL(sourceUrl);
return MikanSubscriptionSourceUrlSchema({
category: SubscriptionCategoryEnum.MikanBangumi,
mikanBangumiId: u.searchParams.get(MIKAN_BANGUMI_ID_QUERY_KEY),
mikanFansubId: u.searchParams.get(MIKAN_FANSUB_ID_QUERY_KEY),
});
}

View File

@@ -1,5 +1,16 @@
import type { GetSubscriptionsQuery } from '@/infra/graphql/gql/graphql';
import { arkValidatorToTypeNarrower } from '@/infra/errors/arktype';
import {
type GetSubscriptionsQuery,
SubscriptionCategoryEnum,
} from '@/infra/graphql/gql/graphql';
import { gql } from '@apollo/client';
import { type } from 'arktype';
import {
MikanSubscriptionSeasonSourceUrlSchema,
extractMikanSubscriptionBangumiSourceUrl,
extractMikanSubscriptionSeasonSourceUrl,
extractMikanSubscriptionSubscriberSourceUrl,
} from './mikan';
export const GET_SUBSCRIPTIONS = gql`
query GetSubscriptions($filters: SubscriptionsFilterInput!, $orderBy: SubscriptionsOrderInput!, $pagination: PaginationInput!) {
@@ -16,6 +27,7 @@ export const GET_SUBSCRIPTIONS = gql`
category
sourceUrl
enabled
credentialId
}
paginationInfo {
total
@@ -25,6 +37,21 @@ export const GET_SUBSCRIPTIONS = gql`
}
`;
export const INSERT_SUBSCRIPTION = gql`
mutation InsertSubscription($data: SubscriptionsInsertInput!) {
subscriptionsCreateOne(data: $data) {
id
createdAt
updatedAt
displayName
category
sourceUrl
enabled
credentialId
}
}
`;
export type SubscriptionDto =
GetSubscriptionsQuery['subscriptions']['nodes'][number];
@@ -67,6 +94,9 @@ query GetSubscriptionDetail ($id: Int!) {
category
sourceUrl
enabled
credential3rd {
id
}
bangumi {
nodes {
createdAt
@@ -89,3 +119,39 @@ query GetSubscriptionDetail ($id: Int!) {
}
}
`;
export const SubscriptionTypedMikanSeasonSchema =
MikanSubscriptionSeasonSourceUrlSchema.and(
type({
credentialId: 'number>0',
})
);
export const SubscriptionTypedMikanBangumiSchema = type({
category: `'${SubscriptionCategoryEnum.MikanBangumi}'`,
sourceUrl: type.string
.atLeastLength(1)
.narrow(
arkValidatorToTypeNarrower(extractMikanSubscriptionBangumiSourceUrl)
),
});
export const SubscriptionTypedMikanSubscriberSchema = type({
category: `'${SubscriptionCategoryEnum.MikanSubscriber}'`,
sourceUrl: type.string
.atLeastLength(1)
.narrow(
arkValidatorToTypeNarrower(extractMikanSubscriptionSubscriberSourceUrl)
),
});
export const SubscriptionTypedSchema = SubscriptionTypedMikanSeasonSchema.or(
SubscriptionTypedMikanBangumiSchema
).or(SubscriptionTypedMikanSubscriberSchema);
export const SubscriptionInsertFormSchema = type({
enabled: 'boolean',
displayName: 'string>0',
}).and(SubscriptionTypedSchema);
export type SubscriptionInsertForm = typeof SubscriptionInsertFormSchema.infer;

View File

@@ -0,0 +1,6 @@
import { Injectable } from '@outposts/injection-js';
@Injectable()
export class MikanService {
mikanBaseUrl = 'https://mikanani.me';
}

View File

@@ -0,0 +1,31 @@
import {
SubscriptionCategoryEnum,
type SubscriptionsInsertInput,
} from '@/infra/graphql/gql/graphql';
import { Injectable, inject } from '@outposts/injection-js';
import { buildMikanSubscriptionSeasonSourceUrl } from '../schema/mikan';
import type { SubscriptionInsertForm } from '../schema/subscriptions';
import { MikanService } from './mikan.service';
@Injectable()
export class SubscriptionService {
private mikan = inject(MikanService);
transformInsertFormToInput(
form: SubscriptionInsertForm
): SubscriptionsInsertInput {
let sourceUrl: string;
if (form.category === SubscriptionCategoryEnum.MikanSeason) {
sourceUrl = buildMikanSubscriptionSeasonSourceUrl(
this.mikan.mikanBaseUrl,
form
).toString();
} else {
sourceUrl = form.sourceUrl;
}
return {
...form,
sourceUrl,
};
}
}