23 Commits

Author SHA1 Message Date
eai04191
8f58fce1b0 🏴󠁧󠁢󠁷󠁬󠁳󠁿 2019-11-16 01:31:13 +09:00
shibafu
3420e053fc Merge pull request #297 from shikorism/fix/admin-menu-in-sp-layout
管理画面へのリンクがモバイルで表示されないバグの修正
2019-11-14 21:15:09 +09:00
shibafu
a01bc6989e 管理画面へのリンクがモバイルで表示されないバグの修正 2019-11-14 00:59:23 +09:00
shibafu
a71aa0c3b6 Merge pull request #296 from shikorism/fix/295-drop-empty-value-tag
空文字列のタグは保存せず捨てる
2019-11-13 00:39:26 +09:00
shibafu
26be8a086e 空文字列のタグは保存せず捨てる 2019-11-09 23:19:28 +09:00
shibafu
af5de3ee14 Merge pull request #294 from eai04191/feature/resolver-dlsite-affiliate-url
DLsiteResolver 新しいアフィリエイトURL構造に対応
2019-11-09 18:40:01 +09:00
eai04191
c8cee80144 冗長な評価を削除 2019-11-09 18:28:47 +09:00
eai04191
d8e170ff85 DLsiteの新しいアフィリエイトURL構造に対応 2019-11-08 05:36:40 +09:00
shibafu
9c101dfb7b Merge pull request #293 from eai04191/feature/resolver-xtube-fix
XtubeResolver 修正
2019-11-05 08:17:34 +09:00
eai04191
12fd228e75 array_mapのcallbackをシンプルにする 2019-11-05 05:59:00 +09:00
eai04191
dc0eb0a548 tagの各要素をtrim()する 2019-11-04 14:24:09 +09:00
eai04191
16ed4482f4 OGPを吐くようになっていたのでOGPResolverでimageを取得するように変更 2019-11-04 14:23:30 +09:00
eai04191
57a847baf5 Update fixture 2019-11-04 14:01:42 +09:00
shibafu
332b6d7dd0 こっちのほうがマシ 2019-10-14 02:24:39 +09:00
shibafu
99a92c6106 チェックインの編集は本人のみ可能
(cherry picked from commit bcb5abb161)
2019-10-14 02:17:02 +09:00
shibafu
4ca6f00c1b Merge pull request #288 from MitarashiDango/feature_tag_list
タグ一覧画面を追加
2019-10-11 00:33:25 +09:00
shibafu
7b8811b894 Merge pull request #281 from shikorism/fix/279-unauthorized-ajax-call
非ログイン状態でいいねしようとした時にログインを促す
2019-10-11 00:31:24 +09:00
MitarashiDango
900e4c94a7 怒られたのでなおした... 2019-09-25 23:46:06 +09:00
MitarashiDango
a434a45e4a タグ一覧の件数カウント条件を検索機能と揃える 2019-09-25 23:39:36 +09:00
MitarashiDango
d5ee59825f 年齢確認ダイアログ表示時のブラーをタグ一覧へ適用させる 2019-09-19 00:09:41 +09:00
MitarashiDango
dd07940aea スマホ向けレイアウトへタグ一覧ボタンを追加 2019-09-16 14:02:19 +09:00
MitarashiDango
78bb7dae28 タグ一覧画面を追加 2019-09-16 13:24:22 +09:00
shibafu
5642e73391 401 Unauthorizedはよくないね 2019-09-12 00:00:34 +09:00
22 changed files with 1027 additions and 328 deletions

View File

@@ -65,6 +65,10 @@ class EjaculationController extends Controller
if (!empty($inputs['tags'])) {
$tags = explode(' ', $inputs['tags']);
foreach ($tags as $tag) {
if ($tag === '') {
continue;
}
$tag = Tag::firstOrCreate(['name' => $tag]);
$tagIds[] = $tag->id;
}
@@ -106,6 +110,8 @@ class EjaculationController extends Controller
{
$ejaculation = Ejaculation::findOrFail($id);
$this->authorize('edit', $ejaculation);
return view('ejaculation.edit')->with(compact('ejaculation'));
}
@@ -113,6 +119,8 @@ class EjaculationController extends Controller
{
$ejaculation = Ejaculation::findOrFail($id);
$this->authorize('edit', $ejaculation);
$inputs = $request->all();
$validator = Validator::make($inputs, [
@@ -147,6 +155,10 @@ class EjaculationController extends Controller
if (!empty($inputs['tags'])) {
$tags = explode(' ', $inputs['tags']);
foreach ($tags as $tag) {
if ($tag === '') {
continue;
}
$tag = Tag::firstOrCreate(['name' => $tag]);
$tagIds[] = $tag->id;
}
@@ -163,6 +175,9 @@ class EjaculationController extends Controller
public function destroy($id)
{
$ejaculation = Ejaculation::findOrFail($id);
$this->authorize('edit', $ejaculation);
$user = User::findOrFail($ejaculation->user_id);
$ejaculation->tags()->detach();
$ejaculation->delete();

View File

@@ -0,0 +1,37 @@
<?php
namespace App\Http\Controllers;
use App\Tag;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
class TagController extends Controller
{
public function index()
{
$tags = Tag::select(DB::raw(
<<<'SQL'
tags.name,
count(*) AS "checkins_count"
SQL
))
->join('ejaculation_tag', 'tags.id', '=', 'ejaculation_tag.tag_id')
->join('ejaculations', 'ejaculations.id', '=', 'ejaculation_tag.ejaculation_id')
->join('users', 'users.id', '=', 'ejaculations.user_id')
->where('ejaculations.is_private', false)
->where(function ($query) {
$query->where('users.is_protected', false);
if (Auth::check()) {
$query->orWhere('users.id', Auth::id());
}
})
->groupBy('tags.name')
->orderByDesc('checkins_count')
->orderBy('tags.name')
->paginate(100);
return view('tag.index', compact('tags'));
}
}

View File

@@ -53,17 +53,19 @@ class DLsiteResolver implements Resolver
public function resolve(string $url): Metadata
{
//アフィリエイトの場合は普通のURLに変換
if (strpos($url, '/dlaf/=/link/') !== false) {
preg_match('~www\.dlsite\.com/(?P<genre>.+)/dlaf/=/link/work/aid/.+/id/(?P<titleId>..\d+)(\.html)?~', $url, $matches);
// ID型
if (preg_match('~/dlaf/=(/.+/.+)?/link/~', $url)) {
preg_match('~www\.dlsite\.com/(?P<genre>.+)/dlaf/=(/.+/.+)?/link/work/aid/(?P<AffiliateId>.+)/id/(?P<titleId>..\d+)(\.html)?~', $url, $matches);
$url = "https://www.dlsite.com/{$matches['genre']}/work/=/product_id/{$matches['titleId']}.html";
}
// URL型
if (strpos($url, '/dlaf/=/aid/') !== false) {
preg_match('~www\.dlsite\.com/.+/dlaf/=/aid/.+/url/(?P<url>.+)~', $url, $matches);
$affiliate_url = urldecode($matches['url']);
if (preg_match('~www\.dlsite\.com/.+/(work|announce)/=/product_id/..\d+(\.html)?~', $affiliate_url, $matches)) {
$url = $affiliate_url;
$affiliateUrl = urldecode($matches['url']);
if (preg_match('~www\.dlsite\.com/.+/(work|announce)/=/product_id/..\d+(\.html)?~', $affiliateUrl, $matches)) {
$url = $affiliateUrl;
} else {
throw new \RuntimeException("アフィリエイト先のリンクがDLsiteのタイトルではありません: $affiliate_url");
throw new \RuntimeException("アフィリエイト先のリンクがDLsiteのタイトルではありません: $affiliateUrl");
}
}

View File

@@ -16,7 +16,7 @@ class MetadataResolver implements Resolver
'~ec\.toranoana\.(jp|shop)/(tora|joshi)(_[rd]+)?/(ec|digi)/item/~' => ToranoanaResolver::class,
'~iwara\.tv/(videos|images)/.*~' => IwaraResolver::class,
'~www\.dlsite\.com/.*/(work|announce)/=/product_id/..\d+(\.html)?~' => DLsiteResolver::class,
'~www\.dlsite\.com/.*/dlaf/=/link/(work|announce)/aid/.+/..\d+(\.html)?~' => DLsiteResolver::class,
'~www\.dlsite\.com/.*/dlaf/=(/.+/.+)?/link/work/aid/.+(/id)?/..\d+(\.html)?~' => DLsiteResolver::class,
'~www\.dlsite\.com/.*/dlaf/=/aid/.+/url/.+~' => DLsiteResolver::class,
'~dlsite\.jp/...tw/..\d+~' => DLsiteResolver::class,
'~www\.pixiv\.net/member_illust\.php\?illust_id=\d+~' => PixivResolver::class,
@@ -33,7 +33,6 @@ class MetadataResolver implements Resolver
'~store\.steampowered\.com/app/\d+~' => SteamResolver::class,
'~www\.xtube\.com/video-watch/.*-\d+$~'=> XtubeResolver::class,
'~ss\.kb10uy\.org/posts/\d+$~' => Kb10uyShortStoryServerResolver::class,
'~(..|www)\.pornhub\.com/view_video\.php\?viewkey=.+$~'=> PornHubResolver::class,
];
public $mimeTypes = [

View File

@@ -1,55 +0,0 @@
<?php
namespace App\MetadataResolver;
use GuzzleHttp\Client;
use Symfony\Component\DomCrawler\Crawler;
class PornHubResolver implements Resolver
{
/**
* @var Client
*/
private $client;
/**
* @var OGPResolver
*/
private $ogpResolver;
public function __construct(Client $client, OGPResolver $ogpResolver)
{
$this->client = $client;
$this->ogpResolver = $ogpResolver;
}
public function resolve(string $url): Metadata
{
// if (preg_match('~www\.xtube\.com/video-watch/.*-(\d+)$~', $url, $matches) !== 1) {
// throw new \RuntimeException("Unmatched URL Pattern: $url");
// }
// $videoid = $matches[1];
$res = $this->client->get($url);
if ($res->getStatusCode() === 200) {
$html = (string) $res->getBody();
$metadata = $this->ogpResolver->parse($html);
$crawler = new Crawler($html);
$js = $crawler->filter('#player script')->text();
if (preg_match('~({.+});~', $js, $matches)) {
$json = $matches[1];
$data = json_decode($json, true);
$metadata->title = $data['video_title'];
$metadata->image = $data['image_url'];
}
$metadata->tags = $crawler->filter('.video-detailed-info a:not(.add-btn-small)')->extract('_text');
return $metadata;
} else {
throw new \RuntimeException("{$res->getStatusCode()}: $url");
}
}
}

View File

@@ -11,10 +11,15 @@ class XtubeResolver implements Resolver
* @var Client
*/
private $client;
/**
* @var OGPResolver
*/
private $ogpResolver;
public function __construct(Client $client)
public function __construct(Client $client, OGPResolver $ogpResolver)
{
$this->client = $client;
$this->ogpResolver = $ogpResolver;
}
public function resolve(string $url): Metadata
@@ -25,17 +30,14 @@ class XtubeResolver implements Resolver
$res = $this->client->get($url);
$html = (string) $res->getBody();
$metadata = new Metadata();
$metadata = $this->ogpResolver->parse($html);
$crawler = new Crawler($html);
// poster URL抽出
$playerConfig = explode("\n", trim($crawler->filter('#playerWrapper script')->last()->text()));
preg_match('~https:\\\/\\\/cdn\d+-s-hw-e5\.xtube\.com\\\/m=(?P<size>.{8})\\\/videos\\\/\d{6}\\\/\d{2}\\\/.{5}-.{4}-\\\/original\\\/\d+\.jpg~', $playerConfig[0], $matches);
$metadata->image = str_replace('\/', '/', $matches[0]);
$metadata->title = trim($crawler->filter('.underPlayerRateForm h1')->text(''));
$metadata->description = trim($crawler->filter('.fullDescription ')->text(''));
$metadata->tags = $crawler->filter('.tagsCategories a')->extract('_text');
$metadata->image = str_replace('m=eSuQ8f', 'm=eaAaaEFb', $metadata->image);
$metadata->image = str_replace('240X180', 'original', $metadata->image);
$metadata->tags = array_map('trim', $crawler->filter('.tagsCategories a')->extract('_text'));
return $metadata;
}

View File

@@ -0,0 +1,27 @@
<?php
namespace App\Policies;
use App\Ejaculation;
use App\User;
use Illuminate\Auth\Access\HandlesAuthorization;
class EjaculationPolicy
{
use HandlesAuthorization;
/**
* Create a new policy instance.
*
* @return void
*/
public function __construct()
{
//
}
public function edit(User $user, Ejaculation $ejaculation): bool
{
return $user->id === $ejaculation->user_id;
}
}

View File

@@ -2,6 +2,8 @@
namespace App\Providers;
use App\Ejaculation;
use App\Policies\EjaculationPolicy;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Gate;
@@ -14,6 +16,7 @@ class AuthServiceProvider extends ServiceProvider
*/
protected $policies = [
'App\Model' => 'App\Policies\ModelPolicy',
Ejaculation::class => EjaculationPolicy::class,
];
/**

View File

@@ -17,6 +17,7 @@
"chart.js": "^2.7.1",
"cross-env": "^5.2.0",
"date-fns": "^1.30.1",
"grapheme-splitter": "^1.0.4",
"husky": "^1.3.1",
"jquery": "^3.2.1",
"js-cookie": "^2.2.0",

View File

@@ -85,6 +85,9 @@ $(() => {
if (xhr.status === 409) {
callback(JSON.parse(xhr.responseText));
return;
} else if (xhr.status === 401) {
alert('いいねするためにはログインしてください。');
return;
}
console.error(xhr);
@@ -98,4 +101,4 @@ $(() => {
$this.siblings(".card-link").removeClass("card-spoiler");
$this.remove();
});
});
});

View File

@@ -1,6 +1,7 @@
import Vue from 'vue';
import TagInput from "./components/TagInput.vue";
import MetadataPreview from './components/MetadataPreview.vue';
import GraphemeSplitter from "grapheme-splitter";
export const bus = new Vue({name: "EventBus"});
@@ -16,6 +17,7 @@ new Vue({
data: {
metadata: null,
metadataLoadState: MetadataLoadState.Inactive,
noteLength: 0
},
components: {
TagInput,
@@ -28,6 +30,16 @@ new Vue({
this.fetchMetadata(linkInput.value);
}
},
watch: {
noteLength: (length: number) => {
const counter = document.querySelector<HTMLElement>(
"#note-character-counter"
);
if (counter) {
counter.innerText = `残り ${500 - length} 文字`;
}
}
},
methods: {
// オカズリンクの変更時
onChangeLink(event: Event) {
@@ -43,6 +55,14 @@ new Vue({
this.fetchMetadata(url);
}
},
onChangeNote(event: Event) {
if (event.target instanceof HTMLTextAreaElement) {
const splitter = new GraphemeSplitter();
this.noteLength = splitter.splitGraphemes(
event.target.value
).length;
}
},
// メタデータの取得
fetchMetadata(url: string) {
this.metadataLoadState = MetadataLoadState.Loading;

View File

@@ -13,3 +13,6 @@ $primary: #e53fb1;
// Components
@import "components/ejaculation";
@import "components/link-card";
// Tag
@import "tag/index";

22
resources/assets/sass/tag/_index.scss vendored Normal file
View File

@@ -0,0 +1,22 @@
.tags {
& > .btn-tag {
width: 100%;
.tag-name {
display: inline-block;
max-width: 80%;
overflow: hidden;
line-height: 40px;
text-overflow: ellipsis;
white-space: nowrap;
vertical-align: middle;
}
.checkins-count {
display: inline-block;
line-height: 40px;
white-space: nowrap;
vertical-align: middle;
}
}
}

View File

@@ -67,8 +67,8 @@
<div class="form-row">
<div class="form-group col-sm-12">
<label for="note"><span class="oi oi-comment-square"></span> ノート</label>
<textarea id="note" name="note" class="form-control {{ $errors->has('note') ? ' is-invalid' : '' }}" rows="4">{{ old('note') ?? $defaults['note'] }}</textarea>
<small class="form-text text-muted">
<textarea id="note" name="note" class="form-control {{ $errors->has('note') ? ' is-invalid' : '' }}" rows="4" v-on:input="onChangeNote">{{ old('note') ?? $defaults['note'] }}</textarea>
<small id="note-character-counter" class="form-text text-muted">
最大 500 文字
</small>
@if ($errors->has('note'))

View File

@@ -68,8 +68,8 @@
<div class="form-row">
<div class="form-group col-sm-12">
<label for="note"><span class="oi oi-comment-square"></span> ノート</label>
<textarea id="note" name="note" class="form-control {{ $errors->has('note') ? ' is-invalid' : '' }}" rows="4">{{ old('note') ?? $ejaculation->note }}</textarea>
<small class="form-text text-muted">
<textarea id="note" name="note" class="form-control {{ $errors->has('note') ? ' is-invalid' : '' }}" rows="4" v-on:input="onChangeNote">{{ old('note') ?? $ejaculation->note }}</textarea>
<small id="note-character-counter" class="form-text text-muted">
最大 500 文字
</small>
@if ($errors->has('note'))

View File

@@ -54,6 +54,9 @@
<a href="{{ route('user.likes', ['name' => Auth::user()->name]) }}" class="dropdown-item">いいね</a>
<div class="dropdown-divider"></div>
<a href="{{ route('setting') }}" class="dropdown-item">設定</a>
@can ('admin')
<a href="{{ route('admin.dashboard') }}" class="dropdown-item">管理</a>
@endcan
<a href="{{ route('logout') }}" class="dropdown-item" onclick="event.preventDefault(); document.getElementById('logout-form').submit();">ログアウト</a>
</div>
</div>
@@ -79,6 +82,9 @@
<li class="nav-item {{ stripos(Route::currentRouteName(), 'user.okazu') === 0 ? 'active' : ''}}">
<a class="nav-link" href="{{ route('user.okazu', ['name' => Auth::user()->name]) }}">オカズ</a>
</li>
<li class="nav-item {{ stripos(Route::currentRouteName(), 'tag') === 0 ? 'active' : ''}}">
<a class="nav-link" href="{{ route('tag') }}">タグ一覧</a>
</li>
{{--<li class="nav-item">
<a class="nav-link" href="{{ route('ranking') }}">ランキング</a>
</li>--}}
@@ -137,6 +143,13 @@
<a class="btn btn-{{ stripos(Route::currentRouteName(), 'user.okazu') === 0 ? 'primary' : 'outline-secondary'}}" href="{{ route('user.okazu', ['name' => Auth::user()->name]) }}" role="button">オカズ</a>
</div>
</div>
<div class="row mt-2">
<div class="col">
<a class="btn btn-{{ stripos(Route::currentRouteName(), 'tag') === 0 ? 'primary' : 'outline-secondary'}}" href="{{ route('tag') }}" role="button">タグ一覧</a>
</div>
<div class="col">
</div>
</div>
{{-- <div class="row mt-2">
<div class="col">
<a class="btn btn-outline-secondary" href="{{ route('ranking') }}">ランキング</a>

View File

@@ -0,0 +1,20 @@
@extends('layouts.base')
@section('title', 'タグ一覧')
@section('content')
<div class="container pb-1">
<h2 class="mb-3">タグ一覧</h2>
<p class="text-secondary">公開チェックインに付けられているタグを、チェックイン数の多い順で表示しています。</p>
<div class="container-fluid">
<div class="row mx-1">
@foreach($tags as $tag)
<div class="col-12 col-lg-6 col-xl-3 py-3 text-break tags">
<a href="{{ route('search', ['q' => $tag->name]) }}" class="btn btn-outline-primary btn-tag" title="{{ $tag->name }}"><span class="tag-name">{{ $tag->name }}</span> <span class="checkins-count">({{ $tag->checkins_count }})</span></a>
</div>
@endforeach
</div>
{{ $tags->links(null, ['className' => 'mt-4 justify-content-center']) }}
</div>
</div>
@endsection

View File

@@ -46,6 +46,8 @@ Route::redirect('/search', '/search/checkin', 301);
Route::get('/search/checkin', 'SearchController@index')->name('search');
Route::get('/search/related-tag', 'SearchController@relatedTag')->name('search.related-tag');
Route::get('/tag', 'TagController@index')->name('tag');
Route::middleware('can:admin')
->namespace('Admin')
->prefix('admin')

View File

@@ -227,7 +227,7 @@ class DLsiteResolverTest extends TestCase
}
}
public function testAffiliateLink()
public function testOldAffiliateLink()
{
$responseText = file_get_contents(__DIR__ . '/../../fixture/DLsite/testHome.html');
@@ -243,6 +243,38 @@ class DLsiteResolverTest extends TestCase
}
}
public function testSnsAffiliateLink()
{
$responseText = file_get_contents(__DIR__ . '/../../fixture/DLsite/testHome.html');
$this->createResolver(DLsiteResolver::class, $responseText);
$metadata = $this->resolver->resolve('https://www.dlsite.com/home/dlaf/=/t/s/link/work/aid/eai04191/id/RJ221761.html');
$this->assertEquals('ひつじ、数えてあげるっ', $metadata->title);
$this->assertEquals('サークル名: Butterfly Dream' . PHP_EOL . '眠れないあなたに彼女が羊を数えてくれる音声です。', $metadata->description);
$this->assertEquals('https://img.dlsite.jp/modpub/images2/work/doujin/RJ222000/RJ221761_img_main.jpg', $metadata->image);
$this->assertEquals(['癒し', 'バイノーラル/ダミヘ', '日常/生活', 'ほのぼの', '恋人同士'], $metadata->tags);
if ($this->shouldUseMock()) {
$this->assertSame('https://www.dlsite.com/home/work/=/product_id/RJ221761.html', (string) $this->handler->getLastRequest()->getUri());
}
}
public function testAffiliateLink()
{
$responseText = file_get_contents(__DIR__ . '/../../fixture/DLsite/testHome.html');
$this->createResolver(DLsiteResolver::class, $responseText);
$metadata = $this->resolver->resolve('https://www.dlsite.com/home/dlaf/=/t/t/link/work/aid/eai04191/id/RJ221761.html');
$this->assertEquals('ひつじ、数えてあげるっ', $metadata->title);
$this->assertEquals('サークル名: Butterfly Dream' . PHP_EOL . '眠れないあなたに彼女が羊を数えてくれる音声です。', $metadata->description);
$this->assertEquals('https://img.dlsite.jp/modpub/images2/work/doujin/RJ222000/RJ221761_img_main.jpg', $metadata->image);
$this->assertEquals(['癒し', 'バイノーラル/ダミヘ', '日常/生活', 'ほのぼの', '恋人同士'], $metadata->tags);
if ($this->shouldUseMock()) {
$this->assertSame('https://www.dlsite.com/home/work/=/product_id/RJ221761.html', (string) $this->handler->getLastRequest()->getUri());
}
}
public function testAffiliateUrl()
{
$responseText = file_get_contents(__DIR__ . '/../../fixture/DLsite/testHome.html');

File diff suppressed because one or more lines are too long

View File

@@ -5,9 +5,8 @@
"module": "es2015",
"moduleResolution": "node",
"strict": true,
"experimentalDecorators": true
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true
},
"include": [
"resources/assets/js/**/*"
]
}
"include": ["resources/assets/js/**/*"]
}

View File

@@ -3357,6 +3357,11 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.2.tgz#6f0952605d0140c1cfdb138ed005775b92d67b02"
integrity sha512-IItsdsea19BoLC7ELy13q1iJFNmd7ofZH5+X/pJr90/nRoPEX0DJo1dHDbgtYWOhJhcCgMDTOw84RZ72q6lB+Q==
grapheme-splitter@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e"
integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==
growly@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081"