Files
RSSHub/lib/utils/attachments.ts
dependabot[bot] c0ebbc9f8a chore(deps-dev): bump eslint-plugin-unicorn from 77.0.0 to 78.0.0 (#23599)
* chore(deps-dev): bump eslint-plugin-unicorn from 77.0.0 to 78.0.0

Bumps [eslint-plugin-unicorn](https://github.com/sindresorhus/eslint-plugin-unicorn) from 77.0.0 to 78.0.0.
- [Release notes](https://github.com/sindresorhus/eslint-plugin-unicorn/releases)
- [Commits](https://github.com/sindresorhus/eslint-plugin-unicorn/compare/v77.0.0...v78.0.0)

---
updated-dependencies:
- dependency-name: eslint-plugin-unicorn
  dependency-version: 78.0.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* style: fix anti-slop/no-unknown-parameters, no-known-value-widening, no-unsafe-dictionary-type, no-unknown-returns and no-object-parameters

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Tony <TonyRL@users.noreply.github.com>
2026-10-17 01:23:46 +08:00

75 lines
2.8 KiB
TypeScript

import type { CheerioAPI } from 'cheerio';
import type { DataItem } from '@/types';
type Attachment = NonNullable<DataItem['attachments']>[number];
const mimeTypes = {
avif: 'image/avif',
gif: 'image/gif',
jpeg: 'image/jpeg',
jpg: 'image/jpeg',
m4a: 'audio/mp4',
m4v: 'video/mp4',
mov: 'video/quicktime',
mp3: 'audio/mpeg',
mp4: 'video/mp4',
oga: 'audio/ogg',
ogg: 'audio/ogg',
ogv: 'video/ogg',
pdf: 'application/pdf',
png: 'image/png',
svg: 'image/svg+xml',
wav: 'audio/wav',
webm: 'video/webm',
webp: 'image/webp',
zip: 'application/zip',
} satisfies Record<string, string>;
const isMimeTypeKey = (key: string): key is keyof typeof mimeTypes => Object.hasOwn(mimeTypes, key);
export const getItemAttachments = (item: DataItem): Attachment[] => {
const attachments = [...(item.attachments || [])];
if (item.enclosure_url && attachments.every((attachment) => attachment.url !== item.enclosure_url)) {
attachments.unshift({
url: item.enclosure_url,
mime_type: item.enclosure_type || 'application/octet-stream',
title: item.enclosure_title,
size_in_bytes: item.enclosure_length,
duration_in_seconds: typeof item.itunes_duration === 'number' ? item.itunes_duration : undefined,
});
}
return attachments;
};
export const extractAttachments = ($: CheerioAPI, item: DataItem, hideImages = false): Attachment[] => {
const attachments = getItemAttachments(item);
const seen = new Set(attachments.map((attachment) => attachment.url));
// Prefer playable media when a reader supports only one enclosure.
const elements = [...$('audio[src], video[src], audio source[src], video source[src]').toArray(), ...$('img[src], a[href]').toArray()];
for (const element of elements) {
const node = $(element);
const source = node.attr(element.tagName === 'a' ? 'href' : 'src');
if (!source) {
continue;
}
let url: URL;
try {
url = new URL(source, item.link);
} catch {
continue;
}
if (!['https:', 'http:'].includes(url.protocol) || seen.has(url.href)) {
continue;
}
const extension = url.pathname.split('.').at(-1)?.toLowerCase();
const declaredType = node.attr('type')?.split(';', 1)[0].trim().toLowerCase();
const mimeType = declaredType && /^(?:audio|video|image|application)\/[a-z0-9][a-z0-9.+-]*$/.test(declaredType) ? declaredType : extension && isMimeTypeKey(extension) ? mimeTypes[extension] : undefined;
if (!mimeType || mimeType === 'application/xhtml+xml' || (hideImages && mimeType.startsWith('image/'))) {
continue;
}
seen.add(url.href);
attachments.push({ url: url.href, mime_type: mimeType });
}
return attachments;
};