From d59d990f3a4e9c198ef6342db898bf25433c0852 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADtor=20de=20Oliveira?= Date: Tue, 27 Oct 2026 20:31:21 -0400 Subject: [PATCH 1/2] Add Bilibili, Youku and NetEase Music adapters --- CHANGELOG.md | 9 + README.md | 2 + src/Adapters/Bilibili/Detectors/Code.php | 33 + .../Bilibili/Detectors/Description.php | 21 + src/Adapters/Bilibili/Detectors/Image.php | 22 + src/Adapters/Bilibili/Detectors/Title.php | 21 + src/Adapters/Bilibili/Extractor.php | 53 + src/Adapters/NetEase/Detectors/AuthorName.php | 29 + src/Adapters/NetEase/Detectors/Code.php | 39 + src/Adapters/NetEase/Extractor.php | 67 + src/Adapters/Youku/Detectors/Code.php | 33 + src/Adapters/Youku/Extractor.php | 28 + src/Embed.php | 10 +- src/Extractor.php | 9 + src/ExtractorFactory.php | 52 +- tests/ChineseProvidersTest.php | 193 + tests/PagesTest.php | 6 + ...album.cf922cf6241903d934f045c39fca77d5.php | 2549 +++++++++++++ ....song.767df0c775ac55440540e297d3818093.php | 3191 +++++++++++++++++ ...ku.com.v_show-id_xntk1mtu4mdq0oa-.html.php | 75 + .../www.bilibili.com.video-bv1gj411x7h7.php | 120 + ...album.cf922cf6241903d934f045c39fca77d5.php | 32 + ....song.767df0c775ac55440540e297d3818093.php | 137 + ...ku.com.v_show-id_xntk1mtu4mdq0oa-.html.php | 35 + .../www.bilibili.com.video-bv1gj411x7h7.php | 35 + 25 files changed, 6783 insertions(+), 18 deletions(-) create mode 100644 src/Adapters/Bilibili/Detectors/Code.php create mode 100644 src/Adapters/Bilibili/Detectors/Description.php create mode 100644 src/Adapters/Bilibili/Detectors/Image.php create mode 100644 src/Adapters/Bilibili/Detectors/Title.php create mode 100644 src/Adapters/Bilibili/Extractor.php create mode 100644 src/Adapters/NetEase/Detectors/AuthorName.php create mode 100644 src/Adapters/NetEase/Detectors/Code.php create mode 100644 src/Adapters/NetEase/Extractor.php create mode 100644 src/Adapters/Youku/Detectors/Code.php create mode 100644 src/Adapters/Youku/Extractor.php create mode 100644 tests/ChineseProvidersTest.php create mode 100644 tests/cache/music.163.com.album.cf922cf6241903d934f045c39fca77d5.php create mode 100644 tests/cache/music.163.com.song.767df0c775ac55440540e297d3818093.php create mode 100644 tests/cache/v.youku.com.v_show-id_xntk1mtu4mdq0oa-.html.php create mode 100644 tests/cache/www.bilibili.com.video-bv1gj411x7h7.php create mode 100644 tests/fixtures/music.163.com.album.cf922cf6241903d934f045c39fca77d5.php create mode 100644 tests/fixtures/music.163.com.song.767df0c775ac55440540e297d3818093.php create mode 100644 tests/fixtures/v.youku.com.v_show-id_xntk1mtu4mdq0oa-.html.php create mode 100644 tests/fixtures/www.bilibili.com.video-bv1gj411x7h7.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ab7191f..b73c6022 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## [4.6.0] - 2026-10-27 +### Added +- Bilibili adapter: `bilibili.com/video/BV...` and `av...` URLs (also `b23.tv` short links) get a `player.bilibili.com` iframe built from the URL. The title, description and image of Bilibili's error page are ignored [#243] +- Youku adapter: `v.youku.com/v_show/id_.html` URLs get a `player.youku.com/embed` iframe built from the URL [#243] +- NetEase Music adapter: songs, albums and playlists on `music.163.com` get an `outchain/player` iframe, and the author is read from the description. URLs with a `#/` route are requested without it [#243] +- `Extractor::normalizeUri()`, used by adapters to rewrite a URL before it is requested + ## [4.5.2] - 2026-10-27 ### Added - oEmbed responses of type `photo` without `html` now produce an `` embed code from their `url`, `width` and `height` [#584] @@ -324,9 +331,11 @@ Full library refactoring. [#572]: https://github.com/oscarotero/Embed/issues/572 [#573]: https://github.com/oscarotero/Embed/issues/573 [#584]: https://github.com/php-embed/Embed/issues/584 +[#243]: https://github.com/php-embed/Embed/issues/243 [#574]: https://github.com/oscarotero/Embed/issues/574 [#577]: https://github.com/oscarotero/Embed/issues/577 +[4.6.0]: https://github.com/php-embed/Embed/compare/v4.5.2...v4.6.0 [4.5.2]: https://github.com/php-embed/Embed/compare/v4.5.1...v4.5.2 [4.5.1]: https://github.com/php-embed/Embed/compare/v4.5.0...v4.5.1 [4.5.0]: https://github.com/php-embed/Embed/compare/v4.4.19...v4.5.0 diff --git a/README.md b/README.md index 7e6679d8..456d2efe 100644 --- a/README.md +++ b/README.md @@ -263,6 +263,8 @@ The `Extractor` class has many `Detectors`. Each detector is responsible to dete So, an adapter is basically an extractor created specifically for a site. It can contains also custom detectors or apis. If you see the `src/Adapters` folder you can see all adapters. +An adapter can also rewrite the URL before it is requested by overriding the static `normalizeUri()` method of its extractor. For example, the NetEase Music adapter turns `music.163.com/#/song?id=1` into `music.163.com/song?id=1`, because the part after `#` is never sent to the server. + If you create an adapter, you need also register to Embed, so it knows in which website needs to use. To do that, there's the `ExtractorFactory` object, that is responsible for instantiate the right extractor for each site. ```php diff --git a/src/Adapters/Bilibili/Detectors/Code.php b/src/Adapters/Bilibili/Detectors/Code.php new file mode 100644 index 00000000..eed088a7 --- /dev/null +++ b/src/Adapters/Bilibili/Detectors/Code.php @@ -0,0 +1,33 @@ + + */ +class Code extends Detector +{ + public function detect(): ?EmbedCode + { + $params = Extractor::parseVideoPath($this->extractor->getUri()->getPath()); + + if ($params === null) { + return parent::detect(); + } + + return new EmbedCode(html('iframe', [ + 'src' => 'https://player.bilibili.com/player.html?'.http_build_query($params + ['autoplay' => 0]), + 'frameborder' => 0, + 'allowfullscreen' => 'true', + 'scrolling' => 'no', + 'width' => 640, + 'height' => 360, + ]), 640, 360); + } +} diff --git a/src/Adapters/Bilibili/Detectors/Description.php b/src/Adapters/Bilibili/Detectors/Description.php new file mode 100644 index 00000000..6b663088 --- /dev/null +++ b/src/Adapters/Bilibili/Detectors/Description.php @@ -0,0 +1,21 @@ + + */ +class Description extends Detector +{ + public function detect(): ?string + { + if ($this->extractor->isErrorPage()) { + return null; + } + + return parent::detect(); + } +} diff --git a/src/Adapters/Bilibili/Detectors/Image.php b/src/Adapters/Bilibili/Detectors/Image.php new file mode 100644 index 00000000..77b7c6af --- /dev/null +++ b/src/Adapters/Bilibili/Detectors/Image.php @@ -0,0 +1,22 @@ + + */ +class Image extends Detector +{ + public function detect(): ?UriInterface + { + if ($this->extractor->isErrorPage()) { + return null; + } + + return parent::detect(); + } +} diff --git a/src/Adapters/Bilibili/Detectors/Title.php b/src/Adapters/Bilibili/Detectors/Title.php new file mode 100644 index 00000000..86448086 --- /dev/null +++ b/src/Adapters/Bilibili/Detectors/Title.php @@ -0,0 +1,21 @@ + + */ +class Title extends Detector +{ + public function detect(): ?string + { + if ($this->extractor->isErrorPage()) { + return null; + } + + return parent::detect(); + } +} diff --git a/src/Adapters/Bilibili/Extractor.php b/src/Adapters/Bilibili/Extractor.php new file mode 100644 index 00000000..e4278624 --- /dev/null +++ b/src/Adapters/Bilibili/Extractor.php @@ -0,0 +1,53 @@ +> + */ +class Extractor extends Base +{ + /** + * Title of the page Bilibili serves when the video is not available, + * for example when the request comes from a restricted region. + */ + private const ERROR_TITLE = '出错啦'; + + public function createCustomDetectors(): array + { + return [ + 'code' => new Detectors\Code($this), + 'title' => new Detectors\Title($this), + 'description' => new Detectors\Description($this), + 'image' => new Detectors\Image($this), + ]; + } + + public function isErrorPage(): bool + { + $title = $this->getDocument()->select('.//head/title')->str(); + + return $title !== null && strpos($title, self::ERROR_TITLE) !== false; + } + + /** + * Returns the player query (bvid or aid) for a video path. + * + * @return array|null + */ + public static function parseVideoPath(string $path): ?array + { + if (preg_match('#^/video/(BV[0-9A-Za-z]{10})(?:/|$)#', $path, $matches) === 1) { + return ['bvid' => $matches[1]]; + } + + if (preg_match('#^/video/av(\d+)(?:/|$)#i', $path, $matches) === 1) { + return ['aid' => $matches[1]]; + } + + return null; + } +} diff --git a/src/Adapters/NetEase/Detectors/AuthorName.php b/src/Adapters/NetEase/Detectors/AuthorName.php new file mode 100644 index 00000000..95807973 --- /dev/null +++ b/src/Adapters/NetEase/Detectors/AuthorName.php @@ -0,0 +1,29 @@ + + */ +class AuthorName extends Detector +{ + public function detect(): ?string + { + $result = parent::detect(); + + if ($result !== null) { + return $result; + } + + $description = $this->extractor->getMetas()->str('description', 'og:description'); + + if ($description !== null && preg_match('/由\s*(.+?)\s*演唱/u', $description, $matches) === 1) { + return $matches[1]; + } + + return null; + } +} diff --git a/src/Adapters/NetEase/Detectors/Code.php b/src/Adapters/NetEase/Detectors/Code.php new file mode 100644 index 00000000..2aa0d189 --- /dev/null +++ b/src/Adapters/NetEase/Detectors/Code.php @@ -0,0 +1,39 @@ + + */ +class Code extends Detector +{ + public function detect(): ?EmbedCode + { + $item = Extractor::parseUri($this->extractor->getUri()); + + if ($item === null) { + return parent::detect(); + } + + $query = http_build_query([ + 'type' => $item[0], + 'id' => $item[1], + 'auto' => 0, + 'height' => 66, + ]); + + return new EmbedCode(html('iframe', [ + 'src' => "https://music.163.com/outchain/player?{$query}", + 'frameborder' => 0, + 'scrolling' => 'no', + 'width' => 330, + 'height' => 86, + ]), 330, 86); + } +} diff --git a/src/Adapters/NetEase/Extractor.php b/src/Adapters/NetEase/Extractor.php new file mode 100644 index 00000000..07e50829 --- /dev/null +++ b/src/Adapters/NetEase/Extractor.php @@ -0,0 +1,67 @@ +> + */ +class Extractor extends Base +{ + /** Player types used by music.163.com/outchain/player */ + private const TYPES = [ + 'song' => 2, + 'album' => 1, + 'playlist' => 0, + ]; + + public function createCustomDetectors(): array + { + return [ + 'code' => new Detectors\Code($this), + 'authorName' => new Detectors\AuthorName($this), + ]; + } + + /** + * NetEase Music URLs like /#/song?id=1 keep the route in the fragment, + * which is never sent to the server. Move it to the path and query. + */ + public static function normalizeUri(UriInterface $uri): UriInterface + { + $fragment = $uri->getFragment(); + + if (preg_match('#^/(?:m/)?(song|album|playlist)\?(.*)$#', $fragment, $matches) !== 1) { + return $uri; + } + + return $uri->withPath('/'.$matches[1])->withQuery($matches[2])->withFragment(''); + } + + /** + * Returns the player type and the id of a song, album or playlist. + * + * @return array{0: int, 1: string}|null + */ + public static function parseUri(UriInterface $uri): ?array + { + $path = $uri->getPath(); + $type = self::TYPES[trim($path, '/')] ?? null; + + if ($type === null) { + return null; + } + + parse_str($uri->getQuery(), $query); + $id = $query['id'] ?? null; + + if (!is_string($id) || preg_match('/^\d+$/', $id) !== 1) { + return null; + } + + return [$type, $id]; + } +} diff --git a/src/Adapters/Youku/Detectors/Code.php b/src/Adapters/Youku/Detectors/Code.php new file mode 100644 index 00000000..0f7850d9 --- /dev/null +++ b/src/Adapters/Youku/Detectors/Code.php @@ -0,0 +1,33 @@ + + */ +class Code extends Detector +{ + public function detect(): ?EmbedCode + { + $id = Extractor::parseVideoPath($this->extractor->getUri()->getPath()); + + if ($id === null) { + return parent::detect(); + } + + return new EmbedCode(html('iframe', [ + 'src' => 'https://player.youku.com/embed/'.rawurlencode($id), + 'frameborder' => 0, + 'allowfullscreen' => 'true', + 'scrolling' => 'no', + 'width' => 640, + 'height' => 360, + ]), 640, 360); + } +} diff --git a/src/Adapters/Youku/Extractor.php b/src/Adapters/Youku/Extractor.php new file mode 100644 index 00000000..e81788ca --- /dev/null +++ b/src/Adapters/Youku/Extractor.php @@ -0,0 +1,28 @@ +> + */ +class Extractor extends Base +{ + public function createCustomDetectors(): array + { + return [ + 'code' => new Detectors\Code($this), + ]; + } + + public static function parseVideoPath(string $path): ?string + { + if (preg_match('#^/v_show/id_([0-9A-Za-z=]+)\.html$#', $path, $matches) === 1) { + return $matches[1]; + } + + return null; + } +} diff --git a/src/Embed.php b/src/Embed.php index 43c619e0..84612312 100644 --- a/src/Embed.php +++ b/src/Embed.php @@ -7,6 +7,7 @@ use Embed\Http\Crawler; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\UriInterface; class Embed { @@ -21,7 +22,7 @@ public function __construct(?Crawler $crawler = null, ?ExtractorFactory $extract public function get(string $url): Extractor { - $request = $this->crawler->createRequest('GET', $url); + $request = $this->crawler->createRequest('GET', $this->normalizeUri($url)); $response = $this->crawler->sendRequest($request); return $this->extract($request, $response); @@ -33,7 +34,7 @@ public function get(string $url): Extractor public function getMulti(string ...$urls): array { $requests = array_map( - fn ($url) => $this->crawler->createRequest('GET', $url), + fn ($url) => $this->crawler->createRequest('GET', $this->normalizeUri($url)), $urls ); @@ -69,6 +70,11 @@ public function setSettings(array $settings): void $this->extractorFactory->setSettings($settings); } + private function normalizeUri(string $url): UriInterface + { + return $this->extractorFactory->normalizeUri($this->crawler->createUri($url)); + } + private function extract(RequestInterface $request, ResponseInterface $response, bool $redirect = true): Extractor { $uri = $this->crawler->getResponseUri($response); diff --git a/src/Extractor.php b/src/Extractor.php index 641968a9..e3bdb8cf 100644 --- a/src/Extractor.php +++ b/src/Extractor.php @@ -239,6 +239,15 @@ public function getResponse(): ResponseInterface return $this->response; } + /** + * Rewrites a URL before it is requested. Adapters can override it, + * for example to move a route stored in the fragment to the path. + */ + public static function normalizeUri(UriInterface $uri): UriInterface + { + return $uri; + } + public function getUri(): UriInterface { return $this->uri; diff --git a/src/ExtractorFactory.php b/src/ExtractorFactory.php index 8e4dd5a7..38643d84 100644 --- a/src/ExtractorFactory.php +++ b/src/ExtractorFactory.php @@ -32,6 +32,9 @@ class ExtractorFactory 'bandcamp.com' => Adapters\Bandcamp\Extractor::class, 'twitter.com' => Adapters\Twitter\Extractor::class, 'x.com' => Adapters\Twitter\Extractor::class, + 'bilibili.com' => Adapters\Bilibili\Extractor::class, + 'youku.com' => Adapters\Youku\Extractor::class, + 'music.163.com' => Adapters\NetEase\Extractor::class, ]; /** @var array>> */ private array $customDetectors = []; @@ -48,22 +51,7 @@ public function __construct(?array $settings = []) public function createExtractor(UriInterface $uri, RequestInterface $request, ResponseInterface $response, Crawler $crawler): Extractor { - $host = $uri->getHost(); - $class = $this->default; - - foreach ($this->adapters as $adapterHost => $adapter) { - // Check if $host is the same domain as $adapterHost. - if ($host === $adapterHost) { - $class = $adapter; - break; - } - - // Check if $host is a subdomain of $adapterHost. - if (substr($host, -strlen($adapterHost) - 1) === ".{$adapterHost}") { - $class = $adapter; - break; - } - } + $class = $this->getClass($uri); $extractor = new $class($uri, $request, $response, $crawler); $extractor->setSettings($this->settings); @@ -80,6 +68,38 @@ public function createExtractor(UriInterface $uri, RequestInterface $request, Re return $extractor; } + /** + * Lets the adapter of the URL host rewrite the URL before it is requested. + */ + public function normalizeUri(UriInterface $uri): UriInterface + { + $class = $this->getClass($uri); + + return $class::normalizeUri($uri); + } + + /** + * @return class-string + */ + private function getClass(UriInterface $uri): string + { + $host = $uri->getHost(); + + foreach ($this->adapters as $adapterHost => $adapter) { + // Check if $host is the same domain as $adapterHost. + if ($host === $adapterHost) { + return $adapter; + } + + // Check if $host is a subdomain of $adapterHost. + if (substr($host, -strlen($adapterHost) - 1) === ".{$adapterHost}") { + return $adapter; + } + } + + return $this->default; + } + /** * @param class-string $class */ diff --git a/tests/ChineseProvidersTest.php b/tests/ChineseProvidersTest.php new file mode 100644 index 00000000..441c41b3 --- /dev/null +++ b/tests/ChineseProvidersTest.php @@ -0,0 +1,193 @@ +出错啦! - bilibili.com' + .''; + + private RecordingClient $client; + + public function testBilibiliBvidBuildsPlayerIframe(): void + { + $info = $this->embed(['/video/BV1GJ411x7h7' => self::BILIBILI_ERROR]) + ->get('https://www.bilibili.com/video/BV1GJ411x7h7/?spm_id_from=333.1007'); + + $this->assertNotNull($info->code); + $this->assertSame( + '', + $info->code->html + ); + $this->assertSame(640, $info->code->width); + $this->assertSame(360, $info->code->height); + } + + public function testBilibiliAidBuildsPlayerIframe(): void + { + $info = $this->embed(['/video/av170001' => self::BILIBILI_ERROR]) + ->get('https://www.bilibili.com/video/av170001'); + + $this->assertNotNull($info->code); + $this->assertStringContainsString('player.html?aid=170001&autoplay=0', $info->code->html); + } + + public function testBilibiliErrorPageHasNoMetadata(): void + { + $info = $this->embed(['/video/BV1GJ411x7h7' => self::BILIBILI_ERROR]) + ->get('https://www.bilibili.com/video/BV1GJ411x7h7'); + + $this->assertNull($info->title); + $this->assertNull($info->description); + $this->assertNull($info->image); + } + + public function testBilibiliVideoPageKeepsMetadata(): void + { + $info = $this->embed([ + '/video/BV1GJ411x7h7' => $this->page( + '' + .'' + ), + ])->get('https://www.bilibili.com/video/BV1GJ411x7h7'); + + $this->assertSame('Video title', $info->title); + $this->assertSame('https://i0.hdslb.com/bfs/archive/cover.jpg', (string) $info->image); + } + + public function testBilibiliOtherPageHasNoPlayer(): void + { + $info = $this->embed(['/' => $this->page('')])->get('https://www.bilibili.com/'); + + $this->assertNull($info->code); + } + + public function testYoukuBuildsPlayerIframe(): void + { + $info = $this->embed(['/v_show/id_XNTk1MTU4MDQ0OA==.html' => $this->page('')]) + ->get('https://v.youku.com/v_show/id_XNTk1MTU4MDQ0OA==.html?spm=a2hja.12701474'); + + $this->assertNotNull($info->code); + $this->assertSame( + '', + $info->code->html + ); + $this->assertSame(640, $info->code->width); + $this->assertSame(360, $info->code->height); + } + + public function testYoukuBlockedRequestStillHasPlayer(): void + { + $info = $this->embed([])->get('https://v.youku.com/v_show/id_XNTk1MTU4MDQ0OA==.html'); + + $this->assertNotNull($info->code); + $this->assertStringContainsString('player.youku.com/embed/', $info->code->html); + } + + /** + * @return array + */ + public function netEaseUrlProvider(): array + { + return [ + ['https://music.163.com/song?id=186016', 2, '186016'], + ['https://music.163.com/#/song?id=186016', 2, '186016'], + ['https://music.163.com/#/album?id=18905', 1, '18905'], + ['https://music.163.com/#/playlist?id=3778678', 0, '3778678'], + ['https://music.163.com/#/m/song?id=186016', 2, '186016'], + ]; + } + + /** + * @dataProvider netEaseUrlProvider + */ + public function testNetEaseBuildsPlayerIframe(string $url, int $type, string $id): void + { + $info = $this->embed([ + '/song' => $this->page(''), + '/album' => $this->page(''), + '/playlist' => $this->page(''), + ])->get($url); + + $this->assertNotNull($info->code); + $this->assertSame( + "", + $info->code->html + ); + $this->assertSame(330, $info->code->width); + $this->assertSame(86, $info->code->height); + } + + public function testNetEaseFragmentRouteIsRequestedAsPath(): void + { + $info = $this->embed(['/song' => $this->page('')])->get('https://music.163.com/#/song?id=186016'); + $urls = array_map(static fn (RequestInterface $request): string => (string) $request->getUri(), $this->client->requests); + + $this->assertSame(['https://music.163.com/song?id=186016'], $urls); + $this->assertSame('https://music.163.com/song?id=186016', (string) $info->url); + } + + public function testNetEaseAuthorFromDescription(): void + { + $info = $this->embed([ + '/song' => $this->page(''), + ])->get('https://music.163.com/song?id=186016'); + + $this->assertSame('周杰伦', $info->authorName); + } + + public function testNetEaseOtherPageHasNoPlayer(): void + { + $info = $this->embed(['/' => $this->page('')])->get('https://music.163.com/'); + + $this->assertNull($info->code); + } + + private function page(string $head): string + { + return 'Page title'.$head.''; + } + + /** + * @param array $routes path => body + */ + private function embed(array $routes): Embed + { + $this->client = $client = new RecordingClient(static function (RequestInterface $request) use ($routes): ResponseInterface { + $factory = new Psr17Factory(); + $body = $routes[$request->getUri()->getPath()] ?? null; + + if ($body === null) { + return $factory->createResponse(403)->withBody($factory->createStream('')); + } + + return $factory->createResponse(200) + ->withHeader('Content-Type', 'text/html') + ->withBody($factory->createStream($body)); + }); + + $crawler = new Crawler($client); + $crawler->setUrlPolicy(UrlPolicy::default()->withResolver(static function (string $host): array { + return ['1.1.1.1']; + })); + + return new Embed($crawler); + } +} diff --git a/tests/PagesTest.php b/tests/PagesTest.php index 441b15b5..96f5eb23 100644 --- a/tests/PagesTest.php +++ b/tests/PagesTest.php @@ -53,6 +53,12 @@ public function urlDataProvider(): array ['http://www.wired.com/?p=2064839'], ['https://www.itmedia.co.jp/news/articles/2410/28/news159.html'], ['https://4pda.to/2022/12/04/406834/sostoyalsya_reliz_clown_of_duty_parodii_na_call_of_duty/'], + // Bilibili and Youku snapshots were captured from a restricted network (a US + // data center IP): Bilibili serves its error page and Youku blocks the request. + ['https://www.bilibili.com/video/BV1GJ411x7h7'], + ['https://v.youku.com/v_show/id_XNTk1MTU4MDQ0OA==.html'], + ['https://music.163.com/song?id=186016'], + ['https://music.163.com/#/album?id=18905'], ]; } diff --git a/tests/cache/music.163.com.album.cf922cf6241903d934f045c39fca77d5.php b/tests/cache/music.163.com.album.cf922cf6241903d934f045c39fca77d5.php new file mode 100644 index 00000000..199a8302 --- /dev/null +++ b/tests/cache/music.163.com.album.cf922cf6241903d934f045c39fca77d5.php @@ -0,0 +1,2549 @@ + [ + 'server' => [ + 'nginx' + ], + 'date' => [ + 'Wed, 28 Oct 2026 00:28:55 GMT' + ], + 'content-type' => [ + 'text/html; charset=utf8' + ], + 'vary' => [ + 'Accept-Encoding' + ], + 'content-security-policy' => [ + 'upgrade-insecure-requests' + ], + 'x-custom-music-web' => [ + 'no' + ], + 'x-traceid-v2' => [ + '72daa22b186eba2d3f350d15bb10ad63^1790043134004^-15324424752' + ], + 'mconfig-bucket' => [ + '999999' + ], + 'x-traceid' => [ + '000001a17fe8f18f057c0a7bbde50007' + ], + 'cache-control' => [ + 'no-cache', + 'no-store' + ], + 'expires' => [ + 'Thu, 01 Jan 1970 00:00:00 GMT' + ], + 'gw-thread' => [ + '-837227' + ], + 'gw-time' => [ + '1793147335056' + ], + 'x-via' => [ + 'MusicServer' + ], + 'content-encoding' => [ + 'br' + ], + 'Content-Location' => [ + 'https://music.163.com/album?id=18905' + ], + 'X-Request-Time' => [ + '1.462 ms' + ] + ], + 'statusCode' => 200, + 'reasonPhrase' => 'OK', + 'body' => ' + + + + + + + + + + + + + +网易云音乐 + + + + + + + + + + + +
 
+
 
+ + +
+
+
+
    +
  • + +云村交易所 +
  • +
  • + +X StudioAI歌手 +
  • +
  • + +用户认证 +
  • +
  • + +天音AI写歌 +
  • +
  • + +云推歌 +
  • +
  • + +赞赏 +
  • +
+
+ +

+廉正举报 +不良信息举报邮箱: 51jubao@service.netease.com +

+

+互联网宗教信息服务许可证:浙(2022)0000120 +增值电信业务经营许可证:浙B2-20150198 +粤B2-20090191-18  浙ICP备15006616号-4  工业和信息化部备案管理系统网站 +

+

+网易公司版权所有©1997-2026杭州乐读科技有限公司运营:浙网文[2024] 0900-042号 + + +浙公网安备 33010802013307号 + + +算法服务公示信息 + +

+
+
+
+
+ + + + + + +' +]; diff --git a/tests/cache/music.163.com.song.767df0c775ac55440540e297d3818093.php b/tests/cache/music.163.com.song.767df0c775ac55440540e297d3818093.php new file mode 100644 index 00000000..00114ecc --- /dev/null +++ b/tests/cache/music.163.com.song.767df0c775ac55440540e297d3818093.php @@ -0,0 +1,3191 @@ + [ + 'server' => [ + 'nginx' + ], + 'date' => [ + 'Wed, 28 Oct 2026 00:28:53 GMT' + ], + 'content-type' => [ + 'text/html; charset=utf8' + ], + 'vary' => [ + 'Accept-Encoding' + ], + 'content-security-policy' => [ + 'upgrade-insecure-requests' + ], + 'x-custom-music-web' => [ + 'no' + ], + 'x-traceid-v2' => [ + '5b4511c695449ad4284f5f2b806d77a5^1791444329318^-8184289037' + ], + 'mconfig-bucket' => [ + '999999' + ], + 'x-traceid' => [ + '000001a17fe8e9e209320ab0f83c0007' + ], + 'cache-control' => [ + 'no-cache', + 'no-store' + ], + 'expires' => [ + 'Thu, 01 Jan 1970 00:00:00 GMT' + ], + 'gw-thread' => [ + '888890' + ], + 'gw-time' => [ + '1793147333090' + ], + 'x-via' => [ + 'MusicServer' + ], + 'content-encoding' => [ + 'br' + ], + 'Content-Location' => [ + 'https://music.163.com/song?id=186016' + ], + 'X-Request-Time' => [ + '2.169 ms' + ] + ], + 'statusCode' => 200, + 'reasonPhrase' => 'OK', + 'body' => ' + + + + + + + + + + + + + + + +晴天(Sunny Day) - 周杰伦 - 单曲 - 网易云音乐 + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
 
+
 
+ +
+
+
+
+
+
+
+
+晴天_周杰伦_叶惠美 | 在线播放_晴天歌词_晴天下载 | 网易云音乐 + +
+ +
+
点击打开客户端
+
+
+
+
+ +
+晴天 +
+Sunny Day +
+
+
+

歌手:周杰伦

+

所属专辑:叶惠美

+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+

+包含这首歌的歌单 +

+ +
+

+网易云音乐多端下载 +

+ +

同步歌单,随时畅听好音乐

+
+

+用户wiki +

+ + +补充或修改歌曲资料 + + + +用户wiki任务中心 + +
+
+
+
+
+
+
    +
  • + +云村交易所 +
  • +
  • + +X StudioAI歌手 +
  • +
  • + +用户认证 +
  • +
  • + +天音AI写歌 +
  • +
  • + +云推歌 +
  • +
  • + +赞赏 +
  • +
+
+ +

+廉正举报 +不良信息举报邮箱: 51jubao@service.netease.com +

+

+互联网宗教信息服务许可证:浙(2022)0000120 +增值电信业务经营许可证:浙B2-20150198 +粤B2-20090191-18  浙ICP备15006616号-4  工业和信息化部备案管理系统网站 +

+

+网易公司版权所有©1997-2026杭州乐读科技有限公司运营:浙网文[2024] 0900-042号 + + +浙公网安备 33010802013307号 + + +算法服务公示信息 + +

+
+
+
+
+ + + + + + +' +]; diff --git a/tests/cache/v.youku.com.v_show-id_xntk1mtu4mdq0oa-.html.php b/tests/cache/v.youku.com.v_show-id_xntk1mtu4mdq0oa-.html.php new file mode 100644 index 00000000..a87a1002 --- /dev/null +++ b/tests/cache/v.youku.com.v_show-id_xntk1mtu4mdq0oa-.html.php @@ -0,0 +1,75 @@ + [ + 'date' => [ + 'Wed, 28 Oct 2026 00:28:51 GMT' + ], + 'content-type' => [ + 'text/html;charset=UTF-8' + ], + 'vary' => [ + 'Accept-Encoding' + ], + 'cache-control' => [ + 'no-store' + ], + 'access-control-allow-credentials' => [ + 'true' + ], + 'bxpunish' => [ + '1' + ], + 'server' => [ + 'Tengine/Aserver' + ], + 'eagleeye-traceid' => [ + '2100c81c17931473316557483e7cd2', + '2100c81c17931473316557483e7cd2' + ], + 'strict-transport-security' => [ + 'max-age=31536000' + ], + 'timing-allow-origin' => [ + '*' + ], + 's-rt' => [ + '86' + ], + 'content-encoding' => [ + 'gzip' + ], + 'Content-Location' => [ + 'https://v.youku.com/v_show/id_XNTk1MTU4MDQ0OA==.html' + ], + 'X-Request-Time' => [ + '1.475 ms' + ] + ], + 'statusCode' => 200, + 'reasonPhrase' => 'OK', + 'body' => ' + + +' +]; diff --git a/tests/cache/www.bilibili.com.video-bv1gj411x7h7.php b/tests/cache/www.bilibili.com.video-bv1gj411x7h7.php new file mode 100644 index 00000000..7a9e2077 --- /dev/null +++ b/tests/cache/www.bilibili.com.video-bv1gj411x7h7.php @@ -0,0 +1,120 @@ + [ + 'date' => [ + 'Wed, 28 Oct 2026 00:28:50 GMT' + ], + 'content-type' => [ + 'text/html' + ], + 'server' => [ + 'openresty' + ], + 'x-sec-request-id' => [ + '07e43c86-e59c-67a3-0000-000031e1594c' + ], + 'Content-Location' => [ + 'https://www.bilibili.com/video/BV1GJ411x7h7' + ], + 'X-Request-Time' => [ + '1.371 ms' + ] + ], + 'statusCode' => 412, + 'reasonPhrase' => 'Precondition Failed', + 'body' => ' + + + + + + + + + + 出错啦! - bilibili.com + + + + + + + +
+
+
+
错误号: 412
+
+
由于触发哔哩哔哩安全风控策略,该次访问请求被拒绝。 +
The request was rejected because of the bilibili security control policy.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + + +' +]; diff --git a/tests/fixtures/music.163.com.album.cf922cf6241903d934f045c39fca77d5.php b/tests/fixtures/music.163.com.album.cf922cf6241903d934f045c39fca77d5.php new file mode 100644 index 00000000..ef2eb70a --- /dev/null +++ b/tests/fixtures/music.163.com.album.cf922cf6241903d934f045c39fca77d5.php @@ -0,0 +1,32 @@ + null, + 'authorUrl' => null, + 'cms' => null, + 'code' => [ + 'html' => '', + 'width' => 330, + 'height' => 86, + 'ratio' => 26.061 + ], + 'description' => null, + 'favicon' => 'https://s1.music.126.net/style/favicon.ico?v20180823', + 'feeds' => [], + 'icon' => null, + 'image' => null, + 'keywords' => [], + 'language' => 'zh-CN', + 'languages' => [], + 'license' => null, + 'providerName' => '网易云音乐', + 'providerUrl' => 'https://music.163.com', + 'publishedTime' => null, + 'redirect' => null, + 'title' => '网易云音乐', + 'url' => 'https://music.163.com/album?id=18905', + 'linkedData' => [], + 'oEmbed' => [], + 'allLinkedData' => [] +]; diff --git a/tests/fixtures/music.163.com.song.767df0c775ac55440540e297d3818093.php b/tests/fixtures/music.163.com.song.767df0c775ac55440540e297d3818093.php new file mode 100644 index 00000000..682cc4d4 --- /dev/null +++ b/tests/fixtures/music.163.com.song.767df0c775ac55440540e297d3818093.php @@ -0,0 +1,137 @@ + '周杰伦', + 'authorUrl' => null, + 'cms' => null, + 'code' => [ + 'html' => '', + 'width' => 330, + 'height' => 86, + 'ratio' => 26.061 + ], + 'description' => '歌曲名《晴天》,别名《Sunny Day》,由 周杰伦 演唱,收录于《叶惠美》专辑中', + 'favicon' => 'https://s1.music.126.net/style/favicon.ico?v20180823', + 'feeds' => [], + 'icon' => null, + 'image' => 'http://p1.music.126.net/ZGffiDQZrGj5s_hnR1CNbg==/109951165566379710.jpg', + 'keywords' => [ + '晴天,sunny day,周杰伦,《晴天》下载,《晴天》歌词,《晴天》在线试听,《晴天》mp3下载,《晴天》无损音乐下载' + ], + 'language' => 'zh-CN', + 'languages' => [], + 'license' => null, + 'providerName' => '网易云音乐', + 'providerUrl' => 'https://music.163.com', + 'publishedTime' => '2003-07-31 00:00:00', + 'redirect' => null, + 'title' => '晴天', + 'url' => 'https://music.163.com/song?id=186016', + 'linkedData' => [ + '@context' => 'https://schema.org', + '@type' => [ + 'MusicRecording', + 'CreativeWork' + ], + 'name' => '晴天', + 'description' => '歌曲名《晴天》,别名《Sunny Day》,由 周杰伦 演唱,收录于《叶惠美》专辑中。《晴天》下载,《晴天》在线试听,更多相关歌曲推荐尽在网易云音乐', + 'image' => 'https://p1.music.126.net/ZGffiDQZrGj5s_hnR1CNbg==/109951165566379710.jpg', + 'byArtist' => [ + [ + '@type' => 'MusicGroup', + 'name' => '周杰伦', + 'url' => 'https://music.163.com/artist?id=6452' + ] + ], + 'inAlbum' => [ + '@type' => 'MusicAlbum', + 'name' => '叶惠美', + 'url' => 'https://music.163.com/album?id=18905', + 'image' => 'https://p1.music.126.net/ZGffiDQZrGj5s_hnR1CNbg==/109951165566379710.jpg' + ], + 'duration' => 'PT4M29S', + 'datePublished' => '2003-07-31T00:00:00', + 'publisher' => [ + '@type' => 'Organization', + 'name' => '网易云音乐', + 'logo' => [ + '@type' => 'ImageObject', + 'url' => 'https://p3.music.126.net/9z9CeujRSPOPm7Rq2DFw_g==/6674035581283071.jpg?param=200y200' + ] + ], + 'mainEntityOfPage' => [ + '@type' => 'WebPage', + '@id' => 'https://music.163.com/song?id=186016' + ] + ], + 'oEmbed' => [], + 'allLinkedData' => [ + [ + '@context' => 'https://ziyuan.baidu.com/contexts/cambrian.jsonld', + '@id' => 'https://music.163.com:80/song?id=186016', + 'appid' => '1582028769404989', + 'title' => '晴天(Sunny Day)', + 'images' => [ + 'http://p1.music.126.net/ZGffiDQZrGj5s_hnR1CNbg==/109951165566379710.jpg' + ], + 'description' => '歌曲名《晴天》,别名《Sunny Day》,由 周杰伦 演唱,收录于《叶惠美》专辑中。《晴天》下载,《晴天》在线试听,更多相关歌曲推荐尽在网易云音乐', + 'pubDate' => '2003-07-31T00:00:00' + ], + [ + '@context' => 'https://schema.org', + '@type' => [ + 'MusicRecording', + 'CreativeWork' + ], + 'name' => '晴天', + 'description' => '歌曲名《晴天》,别名《Sunny Day》,由 周杰伦 演唱,收录于《叶惠美》专辑中。《晴天》下载,《晴天》在线试听,更多相关歌曲推荐尽在网易云音乐', + 'image' => 'https://p1.music.126.net/ZGffiDQZrGj5s_hnR1CNbg==/109951165566379710.jpg', + 'byArtist' => [ + [ + '@type' => 'MusicGroup', + 'name' => '周杰伦', + 'url' => 'https://music.163.com/artist?id=6452' + ] + ], + 'inAlbum' => [ + '@type' => 'MusicAlbum', + 'name' => '叶惠美', + 'url' => 'https://music.163.com/album?id=18905', + 'image' => 'https://p1.music.126.net/ZGffiDQZrGj5s_hnR1CNbg==/109951165566379710.jpg' + ], + 'duration' => 'PT4M29S', + 'datePublished' => '2003-07-31T00:00:00', + 'publisher' => [ + '@type' => 'Organization', + 'name' => '网易云音乐', + 'logo' => [ + '@type' => 'ImageObject', + 'url' => 'https://p3.music.126.net/9z9CeujRSPOPm7Rq2DFw_g==/6674035581283071.jpg?param=200y200' + ] + ], + 'mainEntityOfPage' => [ + '@type' => 'WebPage', + '@id' => 'https://music.163.com/song?id=186016' + ] + ], + [ + '@context' => 'https://schema.org', + '@type' => 'BreadcrumbList', + 'itemListElement' => [ + [ + '@type' => 'ListItem', + 'position' => 1, + 'name' => '首页', + 'item' => 'https://music.163.com/' + ], + [ + '@type' => 'ListItem', + 'position' => 2, + 'name' => '晴天', + 'item' => 'https://music.163.com/song?id=186016' + ] + ] + ] + ] +]; diff --git a/tests/fixtures/v.youku.com.v_show-id_xntk1mtu4mdq0oa-.html.php b/tests/fixtures/v.youku.com.v_show-id_xntk1mtu4mdq0oa-.html.php new file mode 100644 index 00000000..3cae20b9 --- /dev/null +++ b/tests/fixtures/v.youku.com.v_show-id_xntk1mtu4mdq0oa-.html.php @@ -0,0 +1,35 @@ + null, + 'authorUrl' => null, + 'cms' => null, + 'code' => [ + 'html' => '', + 'width' => 640, + 'height' => 360, + 'ratio' => 56.25 + ], + 'description' => null, + 'favicon' => 'https://v.youku.com/favicon.ico', + 'feeds' => [], + 'icon' => null, + 'image' => null, + 'keywords' => [], + 'language' => null, + 'languages' => [], + 'license' => null, + 'providerName' => 'Youku', + 'providerUrl' => 'https://v.youku.com', + 'publishedTime' => null, + 'redirect' => null, + 'title' => null, + 'url' => 'https://v.youku.com/v_show/id_XNTk1MTU4MDQ0OA==.html', + 'linkedData' => [], + 'oEmbed' => [], + 'allLinkedData' => [] +]; diff --git a/tests/fixtures/www.bilibili.com.video-bv1gj411x7h7.php b/tests/fixtures/www.bilibili.com.video-bv1gj411x7h7.php new file mode 100644 index 00000000..14d7d2c8 --- /dev/null +++ b/tests/fixtures/www.bilibili.com.video-bv1gj411x7h7.php @@ -0,0 +1,35 @@ + null, + 'authorUrl' => null, + 'cms' => null, + 'code' => [ + 'html' => '', + 'width' => 640, + 'height' => 360, + 'ratio' => 56.25 + ], + 'description' => null, + 'favicon' => 'https://static.hdslb.com/images/favicon.ico', + 'feeds' => [], + 'icon' => null, + 'image' => null, + 'keywords' => [], + 'language' => 'zh-cn', + 'languages' => [], + 'license' => null, + 'providerName' => 'Bilibili', + 'providerUrl' => 'https://www.bilibili.com', + 'publishedTime' => null, + 'redirect' => null, + 'title' => null, + 'url' => 'https://www.bilibili.com/video/BV1GJ411x7h7', + 'linkedData' => [], + 'oEmbed' => [], + 'allLinkedData' => [] +]; From 3875b22d835823b4d075a2ce0eaa5e638c4b43b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADtor=20de=20Oliveira?= Date: Tue, 27 Oct 2026 20:45:41 -0400 Subject: [PATCH 2/2] Refresh Youku snapshot, handle more Bilibili error pages and NetEase MV URLs --- CHANGELOG.md | 4 +- .../Bilibili/Detectors/Description.php | 9 +- src/Adapters/Bilibili/Extractor.php | 25 +++- src/Adapters/NetEase/Extractor.php | 4 +- tests/ChineseProvidersTest.php | 58 +++++++- tests/PagesTest.php | 4 +- ...ku.com.v_show-id_xntk1mtu4mdq0oa-.html.php | 115 ++++++++++----- ...ku.com.v_show-id_xntk1mtu4mdq0oa-.html.php | 139 ++++++++++++++++-- 8 files changed, 295 insertions(+), 63 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b73c6022..233c5d77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,9 +6,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [4.6.0] - 2026-10-27 ### Added -- Bilibili adapter: `bilibili.com/video/BV...` and `av...` URLs (also `b23.tv` short links) get a `player.bilibili.com` iframe built from the URL. The title, description and image of Bilibili's error page are ignored [#243] +- Bilibili adapter: `bilibili.com/video/BV...` and `av...` URLs (also `b23.tv` short links) get a `player.bilibili.com` iframe built from the URL. The title, description and image of Bilibili's error pages ("出错啦!" and "视频去哪了呢?") are ignored, and so is a description template that was never filled [#243] - Youku adapter: `v.youku.com/v_show/id_.html` URLs get a `player.youku.com/embed` iframe built from the URL [#243] -- NetEase Music adapter: songs, albums and playlists on `music.163.com` get an `outchain/player` iframe, and the author is read from the description. URLs with a `#/` route are requested without it [#243] +- NetEase Music adapter: songs, albums and playlists on `music.163.com` get an `outchain/player` iframe, and the author is read from the description. URLs with a `#/` route are requested without it. MV pages (`/mv?id=` and `/#/mv?id=`) get metadata only, because NetEase has no official embeddable MV player [#243] - `Extractor::normalizeUri()`, used by adapters to rewrite a URL before it is requested ## [4.5.2] - 2026-10-27 diff --git a/src/Adapters/Bilibili/Detectors/Description.php b/src/Adapters/Bilibili/Detectors/Description.php index 6b663088..22677e0a 100644 --- a/src/Adapters/Bilibili/Detectors/Description.php +++ b/src/Adapters/Bilibili/Detectors/Description.php @@ -3,6 +3,7 @@ namespace Embed\Adapters\Bilibili\Detectors; +use Embed\Adapters\Bilibili\Extractor; use Embed\Detectors\Description as Detector; /** @@ -16,6 +17,12 @@ public function detect(): ?string return null; } - return parent::detect(); + $description = parent::detect(); + + if ($description !== null && Extractor::isTemplate($description)) { + return null; + } + + return $description; } } diff --git a/src/Adapters/Bilibili/Extractor.php b/src/Adapters/Bilibili/Extractor.php index e4278624..ac907b7f 100644 --- a/src/Adapters/Bilibili/Extractor.php +++ b/src/Adapters/Bilibili/Extractor.php @@ -11,10 +11,10 @@ class Extractor extends Base { /** - * Title of the page Bilibili serves when the video is not available, + * Titles of the pages Bilibili serves when the video is not available, * for example when the request comes from a restricted region. */ - private const ERROR_TITLE = '出错啦'; + private const ERROR_TITLES = ['出错啦', '视频去哪了呢']; public function createCustomDetectors(): array { @@ -30,7 +30,26 @@ public function isErrorPage(): bool { $title = $this->getDocument()->select('.//head/title')->str(); - return $title !== null && strpos($title, self::ERROR_TITLE) !== false; + if ($title === null) { + return false; + } + + foreach (self::ERROR_TITLES as $errorTitle) { + if (strpos($title, $errorTitle) !== false) { + return true; + } + } + + return false; + } + + /** + * Error pages may also carry a description template that was never + * filled, with placeholders like {$0} or undefined. + */ + public static function isTemplate(string $text): bool + { + return preg_match('/\{\$\d+\}|\bundefined\b/', $text) === 1; } /** diff --git a/src/Adapters/NetEase/Extractor.php b/src/Adapters/NetEase/Extractor.php index 07e50829..c2b4f68f 100644 --- a/src/Adapters/NetEase/Extractor.php +++ b/src/Adapters/NetEase/Extractor.php @@ -29,12 +29,14 @@ public function createCustomDetectors(): array /** * NetEase Music URLs like /#/song?id=1 keep the route in the fragment, * which is never sent to the server. Move it to the path and query. + * MV pages (/mv?id=1) are normalized too, but NetEase has no official + * embeddable MV player, so they only get metadata. */ public static function normalizeUri(UriInterface $uri): UriInterface { $fragment = $uri->getFragment(); - if (preg_match('#^/(?:m/)?(song|album|playlist)\?(.*)$#', $fragment, $matches) !== 1) { + if (preg_match('#^/(?:m/)?(song|album|playlist|mv)\?(.*)$#', $fragment, $matches) !== 1) { return $uri; } diff --git a/tests/ChineseProvidersTest.php b/tests/ChineseProvidersTest.php index 441c41b3..66053c57 100644 --- a/tests/ChineseProvidersTest.php +++ b/tests/ChineseProvidersTest.php @@ -15,15 +15,18 @@ * Bilibili, Youku and NetEase Music adapters [#243]. * * The player iframes are built from the URL alone, so these tests use - * offline pages. The real pages could only be captured from a restricted - * network (a US data center IP), where Bilibili serves its error page and - * Youku blocks the request, so their real metadata is not covered here. + * offline pages. Youku and NetEase real pages are covered by PagesTest + * snapshots captured from a residential connection in Venezuela. Bilibili + * served its error page there too, so its real metadata is not covered. */ class ChineseProvidersTest extends TestCase { private const BILIBILI_ERROR = '出错啦! - bilibili.com' .''; + private const BILIBILI_MISSING = '视频去哪了呢?_哔哩哔哩_bilibili' + .''; + private RecordingClient $client; public function testBilibiliBvidBuildsPlayerIframe(): void @@ -59,6 +62,27 @@ public function testBilibiliErrorPageHasNoMetadata(): void $this->assertNull($info->image); } + public function testBilibiliMissingVideoPageHasNoMetadata(): void + { + $info = $this->embed(['/video/BV1GJ411x7h7' => self::BILIBILI_MISSING]) + ->get('https://www.bilibili.com/video/BV1GJ411x7h7'); + + $this->assertNull($info->title); + $this->assertNull($info->description); + $this->assertNull($info->image); + $this->assertNotNull($info->code); + } + + public function testBilibiliTemplateDescriptionIsDiscarded(): void + { + $info = $this->embed([ + '/video/BV1GJ411x7h7' => $this->page(''), + ])->get('https://www.bilibili.com/video/BV1GJ411x7h7'); + + $this->assertSame('Page title', $info->title); + $this->assertNull($info->description); + } + public function testBilibiliVideoPageKeepsMetadata(): void { $info = $this->embed([ @@ -153,6 +177,34 @@ public function testNetEaseAuthorFromDescription(): void $this->assertSame('周杰伦', $info->authorName); } + /** + * @return array + */ + public function netEaseMvUrlProvider(): array + { + return [ + ['https://music.163.com/mv?id=5436712'], + ['https://music.163.com/#/mv?id=5436712'], + ]; + } + + /** + * NetEase has no official embeddable MV player, so MV pages only get metadata. + * + * @dataProvider netEaseMvUrlProvider + */ + public function testNetEaseMvGetsMetadataOnly(string $url): void + { + $info = $this->embed([ + '/mv' => $this->page(''), + ])->get($url); + $urls = array_map(static fn (RequestInterface $request): string => (string) $request->getUri(), $this->client->requests); + + $this->assertSame(['https://music.163.com/mv?id=5436712'], $urls); + $this->assertSame('广岛之恋', $info->title); + $this->assertNull($info->code); + } + public function testNetEaseOtherPageHasNoPlayer(): void { $info = $this->embed(['/' => $this->page('')])->get('https://music.163.com/'); diff --git a/tests/PagesTest.php b/tests/PagesTest.php index 96f5eb23..8c89640b 100644 --- a/tests/PagesTest.php +++ b/tests/PagesTest.php @@ -53,8 +53,8 @@ public function urlDataProvider(): array ['http://www.wired.com/?p=2064839'], ['https://www.itmedia.co.jp/news/articles/2410/28/news159.html'], ['https://4pda.to/2022/12/04/406834/sostoyalsya_reliz_clown_of_duty_parodii_na_call_of_duty/'], - // Bilibili and Youku snapshots were captured from a restricted network (a US - // data center IP): Bilibili serves its error page and Youku blocks the request. + // Bilibili snapshot comes from a restricted network, where it serves its error + // page. Youku and NetEase were captured from a residential connection in Venezuela. ['https://www.bilibili.com/video/BV1GJ411x7h7'], ['https://v.youku.com/v_show/id_XNTk1MTU4MDQ0OA==.html'], ['https://music.163.com/song?id=186016'], diff --git a/tests/cache/v.youku.com.v_show-id_xntk1mtu4mdq0oa-.html.php b/tests/cache/v.youku.com.v_show-id_xntk1mtu4mdq0oa-.html.php index a87a1002..8067daa3 100644 --- a/tests/cache/v.youku.com.v_show-id_xntk1mtu4mdq0oa-.html.php +++ b/tests/cache/v.youku.com.v_show-id_xntk1mtu4mdq0oa-.html.php @@ -4,72 +4,107 @@ return [ 'headers' => [ 'date' => [ - 'Wed, 28 Oct 2026 00:28:51 GMT' + 'Wed, 28 Oct 2026 00:44:42 GMT' ], 'content-type' => [ - 'text/html;charset=UTF-8' + 'text/html; charset=utf-8' ], 'vary' => [ 'Accept-Encoding' ], - 'cache-control' => [ - 'no-store' + 'x-server-id' => [ + 'e339c9461b97c02b1a60114e0fac7ba996e5c171ea4e47c527ff2c52d4b9f6c2be93192261c5c71d' ], - 'access-control-allow-credentials' => [ - 'true' + 'x-frame-options' => [ + 'SAMEORIGIN' ], - 'bxpunish' => [ - '1' + 'x-xss-protection' => [ + '1; mode=block' ], - 'server' => [ - 'Tengine/Aserver' + 'x-content-type-options' => [ + 'nosniff' ], - 'eagleeye-traceid' => [ - '2100c81c17931473316557483e7cd2', - '2100c81c17931473316557483e7cd2' + 'x-download-options' => [ + 'noopen' ], 'strict-transport-security' => [ + 'max-age=31536000', 'max-age=31536000' ], + 'x-readtime' => [ + '42' + ], + 'content-encoding' => [ + 'gzip' + ], + 'server' => [ + 'Tengine/Aserver' + ], + 'eagleeye-traceid' => [ + '2100cfab17931482819038336e2d98', + '2100cfab17931482819038336e2d98' + ], 'timing-allow-origin' => [ '*' ], 's-rt' => [ - '86' - ], - 'content-encoding' => [ - 'gzip' + '146' ], 'Content-Location' => [ 'https://v.youku.com/v_show/id_XNTk1MTU4MDQ0OA==.html' ], 'X-Request-Time' => [ - '1.475 ms' + '1.371 ms' ] ], 'statusCode' => 200, 'reasonPhrase' => 'OK', 'body' => ' - - -' +田宝申盟新婚快乐-其他-高清完整正版视频在线观看-优酷
    历史
    客户端
    登录
    ' ]; diff --git a/tests/fixtures/v.youku.com.v_show-id_xntk1mtu4mdq0oa-.html.php b/tests/fixtures/v.youku.com.v_show-id_xntk1mtu4mdq0oa-.html.php index 3cae20b9..cc10ddd4 100644 --- a/tests/fixtures/v.youku.com.v_show-id_xntk1mtu4mdq0oa-.html.php +++ b/tests/fixtures/v.youku.com.v_show-id_xntk1mtu4mdq0oa-.html.php @@ -1,8 +1,8 @@ null, @@ -14,22 +14,139 @@ 'height' => 360, 'ratio' => 56.25 ], - 'description' => null, - 'favicon' => 'https://v.youku.com/favicon.ico', + 'description' => '田宝申盟新婚快乐 是在优酷播出的其他高清视频,于2023-03-19 17:35:28上线。视频内容简介:田宝申盟新婚快乐', + 'favicon' => 'https://img.alicdn.com/imgextra/i2/O1CN01BeAcgL1ywY0G5nSn8_!!6000000006643-2-tps-195-195.png', 'feeds' => [], 'icon' => null, 'image' => null, - 'keywords' => [], - 'language' => null, + 'keywords' => [ + '优酷视频,优酷,田宝申盟新婚快乐 其他', + '田宝申盟新婚快乐 其他' + ], + 'language' => 'zh_CN', 'languages' => [], 'license' => null, - 'providerName' => 'Youku', + 'providerName' => '优酷', 'providerUrl' => 'https://v.youku.com', - 'publishedTime' => null, + 'publishedTime' => '2023-03-19 17:35:28', 'redirect' => null, - 'title' => null, + 'title' => '田宝申盟新婚快乐', 'url' => 'https://v.youku.com/v_show/id_XNTk1MTU4MDQ0OA==.html', - 'linkedData' => [], + 'linkedData' => [ + '@context' => 'https://schema.org', + '@graph' => [ + [ + '@type' => 'VideoObject', + 'name' => '田宝申盟新婚快乐-其他-高清完整正版视频在线观看-优酷', + 'description' => '田宝申盟新婚快乐 是在优酷播出的其他高清视频,于2023-03-19 17:35:28上线。视频内容简介:田宝申盟新婚快乐', + 'thumbnailUrl' => '', + 'uploadDate' => '2023-03-19 17:35:28', + 'datePublished' => '2023-03-19 17:35:28', + 'duration' => 'PT0M0S', + 'contentUrl' => 'https://v.youku.com/v_show/id_XNTk1MTU4MDQ0OA==.html?vid=XNTk1MTU4MDQ0OA==', + 'embedUrl' => 'https://v.youku.com/v_show/id_XNTk1MTU4MDQ0OA==.html?vid=XNTk1MTU4MDQ0OA==', + 'genre' => '其他', + 'inLanguage' => 'zh-CN', + 'regionsAllowed' => 'CN', + 'isAccessibleForFree' => false, + 'keywords' => '田宝申盟新婚快乐 其他', + 'publisher' => [ + '@type' => 'Organization', + 'name' => '优酷', + 'logo' => [ + '@type' => 'ImageObject', + 'url' => 'https://img.alicdn.com/imgextra/i2/O1CN01BeAcgL1ywY0G5nSn8_!!6000000006643-2-tps-195-195.png' + ] + ] + ], + [ + '@type' => 'BreadcrumbList', + 'itemListElement' => [ + [ + '@type' => 'ListItem', + 'position' => 1, + 'name' => '优酷', + 'item' => 'https://www.youku.com' + ], + [ + '@type' => 'ListItem', + 'position' => 2, + 'name' => '其他', + 'item' => 'https://www.youku.com/channel/其他' + ], + [ + '@type' => 'ListItem', + 'position' => 3, + 'name' => '田宝申盟新婚快乐' + ], + [ + '@type' => 'ListItem', + 'position' => 4, + 'name' => '田宝申盟新婚快乐-其他-高清完整正版视频在线观看-优酷', + 'item' => 'https://v.youku.com/v_show/id_XNTk1MTU4MDQ0OA==.html?vid=XNTk1MTU4MDQ0OA==' + ] + ] + ] + ] + ], 'oEmbed' => [], - 'allLinkedData' => [] + 'allLinkedData' => [ + [ + '@context' => 'https://schema.org', + '@graph' => [ + [ + '@type' => 'VideoObject', + 'name' => '田宝申盟新婚快乐-其他-高清完整正版视频在线观看-优酷', + 'description' => '田宝申盟新婚快乐 是在优酷播出的其他高清视频,于2023-03-19 17:35:28上线。视频内容简介:田宝申盟新婚快乐', + 'thumbnailUrl' => '', + 'uploadDate' => '2023-03-19 17:35:28', + 'datePublished' => '2023-03-19 17:35:28', + 'duration' => 'PT0M0S', + 'contentUrl' => 'https://v.youku.com/v_show/id_XNTk1MTU4MDQ0OA==.html?vid=XNTk1MTU4MDQ0OA==', + 'embedUrl' => 'https://v.youku.com/v_show/id_XNTk1MTU4MDQ0OA==.html?vid=XNTk1MTU4MDQ0OA==', + 'genre' => '其他', + 'inLanguage' => 'zh-CN', + 'regionsAllowed' => 'CN', + 'isAccessibleForFree' => false, + 'keywords' => '田宝申盟新婚快乐 其他', + 'publisher' => [ + '@type' => 'Organization', + 'name' => '优酷', + 'logo' => [ + '@type' => 'ImageObject', + 'url' => 'https://img.alicdn.com/imgextra/i2/O1CN01BeAcgL1ywY0G5nSn8_!!6000000006643-2-tps-195-195.png' + ] + ] + ], + [ + '@type' => 'BreadcrumbList', + 'itemListElement' => [ + [ + '@type' => 'ListItem', + 'position' => 1, + 'name' => '优酷', + 'item' => 'https://www.youku.com' + ], + [ + '@type' => 'ListItem', + 'position' => 2, + 'name' => '其他', + 'item' => 'https://www.youku.com/channel/其他' + ], + [ + '@type' => 'ListItem', + 'position' => 3, + 'name' => '田宝申盟新婚快乐' + ], + [ + '@type' => 'ListItem', + 'position' => 4, + 'name' => '田宝申盟新婚快乐-其他-高清完整正版视频在线观看-优酷', + 'item' => 'https://v.youku.com/v_show/id_XNTk1MTU4MDQ0OA==.html?vid=XNTk1MTU4MDQ0OA==' + ] + ] + ] + ] + ] + ] ];