commit
3f9de593d6
@ -4,6 +4,7 @@ namespace App\Http\Controllers\Api;
|
|||||||
|
|
||||||
use App\Metadata;
|
use App\Metadata;
|
||||||
use App\MetadataResolver\MetadataResolver;
|
use App\MetadataResolver\MetadataResolver;
|
||||||
|
use App\Tag;
|
||||||
use App\Utilities\Formatter;
|
use App\Utilities\Formatter;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
@ -41,6 +42,13 @@ class CardController
|
|||||||
'image' => $resolved->image,
|
'image' => $resolved->image,
|
||||||
'expires_at' => $resolved->expires_at
|
'expires_at' => $resolved->expires_at
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$tagIds = [];
|
||||||
|
foreach ($resolved->tags as $tagName) {
|
||||||
|
$tag = Tag::firstOrCreate(['name' => $tagName]);
|
||||||
|
$tagIds[] = $tag->id;
|
||||||
|
}
|
||||||
|
$metadata->tags()->sync($tagIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
$response = response($metadata);
|
$response = response($metadata);
|
||||||
|
@ -5,6 +5,7 @@ namespace App\Listeners;
|
|||||||
use App\Events\LinkDiscovered;
|
use App\Events\LinkDiscovered;
|
||||||
use App\Metadata;
|
use App\Metadata;
|
||||||
use App\MetadataResolver\MetadataResolver;
|
use App\MetadataResolver\MetadataResolver;
|
||||||
|
use App\Tag;
|
||||||
use App\Utilities\Formatter;
|
use App\Utilities\Formatter;
|
||||||
use GuzzleHttp\Exception\TransferException;
|
use GuzzleHttp\Exception\TransferException;
|
||||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
@ -47,12 +48,19 @@ class LinkCollector
|
|||||||
if ($metadata == null || ($metadata->expires_at !== null && $metadata->expires_at < now())) {
|
if ($metadata == null || ($metadata->expires_at !== null && $metadata->expires_at < now())) {
|
||||||
try {
|
try {
|
||||||
$resolved = $this->metadataResolver->resolve($url);
|
$resolved = $this->metadataResolver->resolve($url);
|
||||||
Metadata::updateOrCreate(['url' => $url], [
|
$metadata = Metadata::updateOrCreate(['url' => $url], [
|
||||||
'title' => $resolved->title,
|
'title' => $resolved->title,
|
||||||
'description' => $resolved->description,
|
'description' => $resolved->description,
|
||||||
'image' => $resolved->image,
|
'image' => $resolved->image,
|
||||||
'expires_at' => $resolved->expires_at
|
'expires_at' => $resolved->expires_at
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$tagIds = [];
|
||||||
|
foreach ($resolved->tags as $tagName) {
|
||||||
|
$tag = Tag::firstOrCreate(['name' => $tagName]);
|
||||||
|
$tagIds[] = $tag->id;
|
||||||
|
}
|
||||||
|
$metadata->tags()->sync($tagIds);
|
||||||
} catch (TransferException $e) {
|
} catch (TransferException $e) {
|
||||||
// 何らかの通信エラーによってメタデータの取得に失敗した時、とりあえずエラーログにURLを残す
|
// 何らかの通信エラーによってメタデータの取得に失敗した時、とりあえずエラーログにURLを残す
|
||||||
Log::error(self::class . ': メタデータの取得に失敗 URL=' . $url);
|
Log::error(self::class . ': メタデータの取得に失敗 URL=' . $url);
|
||||||
|
@ -14,4 +14,9 @@ class Metadata extends Model
|
|||||||
protected $visible = ['url', 'title', 'description', 'image', 'expires_at'];
|
protected $visible = ['url', 'title', 'description', 'image', 'expires_at'];
|
||||||
|
|
||||||
protected $dates = ['created_at', 'updated_at', 'expires_at'];
|
protected $dates = ['created_at', 'updated_at', 'expires_at'];
|
||||||
|
|
||||||
|
public function tags()
|
||||||
|
{
|
||||||
|
return $this->belongsToMany(Tag::class)->withTimestamps();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -34,6 +34,20 @@ class KomifloResolver implements Resolver
|
|||||||
($json['content']['parents'][0]['data']['title'] ?? '?');
|
($json['content']['parents'][0]['data']['title'] ?? '?');
|
||||||
$metadata->image = 'https://t.komiflo.com/564_mobile_large_3x/' . $json['content']['named_imgs']['cover']['filename'];
|
$metadata->image = 'https://t.komiflo.com/564_mobile_large_3x/' . $json['content']['named_imgs']['cover']['filename'];
|
||||||
|
|
||||||
|
// 作者情報
|
||||||
|
if (!empty($json['content']['attributes']['artists']['children'])) {
|
||||||
|
foreach ($json['content']['attributes']['artists']['children'] as $artist) {
|
||||||
|
$metadata->tags[] = preg_replace('/\s/', '_', $artist['data']['name']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// タグ
|
||||||
|
if (!empty($json['content']['attributes']['tags']['children'])) {
|
||||||
|
foreach ($json['content']['attributes']['tags']['children'] as $tag) {
|
||||||
|
$metadata->tags[] = preg_replace('/\s/', '_', $tag['data']['name']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return $metadata;
|
return $metadata;
|
||||||
} else {
|
} else {
|
||||||
throw new \RuntimeException("{$res->getStatusCode()}: $url");
|
throw new \RuntimeException("{$res->getStatusCode()}: $url");
|
||||||
|
@ -6,9 +6,21 @@ use Carbon\Carbon;
|
|||||||
|
|
||||||
class Metadata
|
class Metadata
|
||||||
{
|
{
|
||||||
|
/** @var string タイトル */
|
||||||
public $title = '';
|
public $title = '';
|
||||||
|
|
||||||
|
/** @var string 概要 */
|
||||||
public $description = '';
|
public $description = '';
|
||||||
|
|
||||||
|
/** @var string サムネイルのURL */
|
||||||
public $image = '';
|
public $image = '';
|
||||||
/** @var Carbon|null */
|
|
||||||
|
/** @var Carbon|null メタデータの有効期限 */
|
||||||
public $expires_at = null;
|
public $expires_at = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string[] タグ
|
||||||
|
* チェックインタグと同様に保存されるため、スペースや改行文字を含めてはいけません。
|
||||||
|
*/
|
||||||
|
public $tags = [];
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,7 @@ class MetadataResolver implements Resolver
|
|||||||
'~www\.melonbooks\.co\.jp/detail/detail\.php~' => MelonbooksResolver::class,
|
'~www\.melonbooks\.co\.jp/detail/detail\.php~' => MelonbooksResolver::class,
|
||||||
'~ec\.toranoana\.(jp|shop)/(tora|joshi)(_[rd]+)?/(ec|digi)/item/~' => ToranoanaResolver::class,
|
'~ec\.toranoana\.(jp|shop)/(tora|joshi)(_[rd]+)?/(ec|digi)/item/~' => ToranoanaResolver::class,
|
||||||
'~iwara\.tv/videos/.*~' => IwaraResolver::class,
|
'~iwara\.tv/videos/.*~' => IwaraResolver::class,
|
||||||
'~www\.dlsite\.com/.*/work/=/product_id/..\d+\.html~' => DLsiteResolver::class,
|
'~www\.dlsite\.com/.*/work/=/product_id/..\d+(\.html)?~' => DLsiteResolver::class,
|
||||||
'~dlsite\.jp/mawtw/..\d+~' => DLsiteResolver::class,
|
'~dlsite\.jp/mawtw/..\d+~' => DLsiteResolver::class,
|
||||||
'~www\.pixiv\.net/member_illust\.php\?illust_id=\d+~' => PixivResolver::class,
|
'~www\.pixiv\.net/member_illust\.php\?illust_id=\d+~' => PixivResolver::class,
|
||||||
'~fantia\.jp/posts/\d+~' => FantiaResolver::class,
|
'~fantia\.jp/posts/\d+~' => FantiaResolver::class,
|
||||||
|
@ -49,6 +49,43 @@ class PixivResolver implements Resolver
|
|||||||
return str_replace('i.pximg.net', 'i.pixiv.cat', $pixivUrl);
|
return str_replace('i.pximg.net', 'i.pixiv.cat', $pixivUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTMLからタグとして利用可能な情報を抽出する
|
||||||
|
* @param string $html ページ HTML
|
||||||
|
* @return string[] タグ
|
||||||
|
*/
|
||||||
|
public function extractTags(string $html): array
|
||||||
|
{
|
||||||
|
$dom = new \DOMDocument();
|
||||||
|
@$dom->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));
|
||||||
|
$xpath = new \DOMXPath($dom);
|
||||||
|
|
||||||
|
$nodes = $xpath->query("//meta[@name='keywords']");
|
||||||
|
if ($nodes->length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$keywords = $nodes->item(0)->getAttribute('content');
|
||||||
|
$tags = [];
|
||||||
|
|
||||||
|
foreach (mb_split(',', $keywords) as $keyword) {
|
||||||
|
$keyword = trim($keyword);
|
||||||
|
|
||||||
|
if (empty($keyword)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 一部の固定キーワードは無視
|
||||||
|
if (array_search($keyword, ['R-18', 'イラスト', 'pixiv', 'ピクシブ'], true)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$tags[] = preg_replace('/\s/', '_', $keyword);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $tags;
|
||||||
|
}
|
||||||
|
|
||||||
public function resolve(string $url): Metadata
|
public function resolve(string $url): Metadata
|
||||||
{
|
{
|
||||||
parse_str(parse_url($url, PHP_URL_QUERY), $params);
|
parse_str(parse_url($url, PHP_URL_QUERY), $params);
|
||||||
@ -78,6 +115,8 @@ class PixivResolver implements Resolver
|
|||||||
|
|
||||||
$metadata->image = $this->proxize($illustUrl);
|
$metadata->image = $this->proxize($illustUrl);
|
||||||
|
|
||||||
|
$metadata->tags = $this->extractTags($res->getBody());
|
||||||
|
|
||||||
return $metadata;
|
return $metadata;
|
||||||
} else {
|
} else {
|
||||||
throw new \RuntimeException("{$res->getStatusCode()}: $url");
|
throw new \RuntimeException("{$res->getStatusCode()}: $url");
|
||||||
|
@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
class CreateMetadataTagTable extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::create('metadata_tag', function (Blueprint $table) {
|
||||||
|
$table->increments('id');
|
||||||
|
$table->text('metadata_url')->index();
|
||||||
|
$table->integer('tag_id')->index();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('metadata_tag');
|
||||||
|
}
|
||||||
|
}
|
8
resources/assets/js/checkin.js
vendored
8
resources/assets/js/checkin.js
vendored
@ -33,9 +33,11 @@ $('#tagInput').on('keydown', function (ev) {
|
|||||||
case 'Tab':
|
case 'Tab':
|
||||||
case 'Enter':
|
case 'Enter':
|
||||||
case ' ':
|
case ' ':
|
||||||
insertTag($this.val().trim());
|
if (ev.originalEvent.isComposing !== true) {
|
||||||
$this.val('');
|
insertTag($this.val().trim());
|
||||||
updateTags();
|
$this.val('');
|
||||||
|
updateTags();
|
||||||
|
}
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -4157,9 +4157,9 @@ isobject@^3.0.0, isobject@^3.0.1:
|
|||||||
integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8=
|
integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8=
|
||||||
|
|
||||||
jquery@^3.2.1:
|
jquery@^3.2.1:
|
||||||
version "3.3.1"
|
version "3.4.1"
|
||||||
resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.3.1.tgz#958ce29e81c9790f31be7792df5d4d95fc57fbca"
|
resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.4.1.tgz#714f1f8d9dde4bdfa55764ba37ef214630d80ef2"
|
||||||
integrity sha512-Ubldcmxp5np52/ENotGxlLe6aGMvmF4R8S6tZjsP6Knsaxd/xp3Zrh50cG93lR6nPXyUFwzN3ZSOQI0wRJNdGg==
|
integrity sha512-36+AdBzCL+y6qjw5Tx7HgzeGCzC81MDDgaUP8ld2zhx58HdqXGoBd+tHdrBMiyjGQs0Hxs/MLZTu/eHNJJuWPw==
|
||||||
|
|
||||||
js-cookie@^2.2.0:
|
js-cookie@^2.2.0:
|
||||||
version "2.2.0"
|
version "2.2.0"
|
||||||
|
Loading…
Reference in New Issue
Block a user