mirror of
https://github.com/NNTmux/newznab-tmux.git
synced 2026-08-28 23:01:29 +00:00
Add missing files
This commit is contained in:
@@ -0,0 +1,232 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Category;
|
||||
use App\Models\Release;
|
||||
use App\Services\Releases\ReleaseBrowseService;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class SeriesReleaseService
|
||||
{
|
||||
private const FALLBACK_SCAN_LIMIT = 500;
|
||||
|
||||
public function __construct(
|
||||
private readonly EpisodeHydrationService $episodeHydrationService,
|
||||
private readonly ReleaseBrowseService $releaseBrowseService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param array<int, int> $categories
|
||||
* @return array<int, int>
|
||||
*/
|
||||
public function seasonCounts(int $videoId, array $categories): array
|
||||
{
|
||||
$counts = $this->baseReleaseQuery($videoId, $categories)
|
||||
->join('tv_episodes as tve', 'r.tv_episodes_id', '=', 'tve.id')
|
||||
->where('tve.videos_id', $videoId)
|
||||
->selectRaw('tve.series as season, COUNT(*) as release_count')
|
||||
->groupBy('tve.series')
|
||||
->orderBy('tve.series')
|
||||
->pluck('release_count', 'season')
|
||||
->mapWithKeys(static fn ($count, $season): array => [(int) $season => (int) $count])
|
||||
->all();
|
||||
|
||||
foreach ($this->fallbackSeasonCounts($videoId, $categories) as $season => $count) {
|
||||
$counts[$season] = ($counts[$season] ?? 0) + $count;
|
||||
}
|
||||
|
||||
ksort($counts);
|
||||
|
||||
return $counts;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, int> $categories
|
||||
* @return array{releases:EloquentCollection<int, Release>, total:int}
|
||||
*/
|
||||
public function releasesForSeason(int $videoId, int $season, int $offset, int $limit, array $categories): array
|
||||
{
|
||||
$matchedCount = $this->matchedSeasonCount($videoId, $season, $categories);
|
||||
$fallback = $this->fallbackReleasesForSeason($videoId, $season, $categories);
|
||||
$fallbackCount = $fallback->count();
|
||||
|
||||
$matchedLimit = $limit;
|
||||
$matchedOffset = $offset;
|
||||
$fallbackSlice = new EloquentCollection;
|
||||
|
||||
if ($limit > 0) {
|
||||
if ($offset >= $matchedCount) {
|
||||
$matchedLimit = 0;
|
||||
$matchedOffset = 0;
|
||||
$fallbackSlice = new EloquentCollection(
|
||||
$fallback->slice($offset - $matchedCount, $limit)->values()->all()
|
||||
);
|
||||
} elseif (($offset + $limit) > $matchedCount) {
|
||||
$matchedLimit = $matchedCount - $offset;
|
||||
$fallbackLimit = $limit - $matchedLimit;
|
||||
$fallbackSlice = new EloquentCollection($fallback->take($fallbackLimit)->values()->all());
|
||||
}
|
||||
}
|
||||
|
||||
$matched = $matchedLimit > 0
|
||||
? $this->matchedSeasonReleaseQuery($videoId, $season, $categories)
|
||||
->orderByDesc('r.postdate')
|
||||
->limit($matchedLimit)
|
||||
->offset($matchedOffset)
|
||||
->get()
|
||||
: new EloquentCollection;
|
||||
|
||||
$releases = new EloquentCollection($matched->concat($fallbackSlice)->values()->all());
|
||||
if ($releases->isNotEmpty()) {
|
||||
$releases->loadCount('failed');
|
||||
}
|
||||
|
||||
return [
|
||||
'releases' => $releases,
|
||||
'total' => $matchedCount + $fallbackCount,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, int> $categories
|
||||
*/
|
||||
private function matchedSeasonCount(int $videoId, int $season, array $categories): int
|
||||
{
|
||||
return $this->baseReleaseQuery($videoId, $categories)
|
||||
->join('tv_episodes as tve', 'r.tv_episodes_id', '=', 'tve.id')
|
||||
->where('tve.videos_id', $videoId)
|
||||
->where('tve.series', $season)
|
||||
->count();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, int> $categories
|
||||
* @return Builder<Release>
|
||||
*/
|
||||
private function matchedSeasonReleaseQuery(int $videoId, int $season, array $categories): Builder
|
||||
{
|
||||
return $this->baseReleaseQuery($videoId, $categories)
|
||||
->join('tv_episodes as tve', 'r.tv_episodes_id', '=', 'tve.id')
|
||||
->where('tve.videos_id', $videoId)
|
||||
->where('tve.series', $season)
|
||||
->select([
|
||||
'r.id',
|
||||
'r.searchname',
|
||||
'r.guid',
|
||||
'r.postdate',
|
||||
'r.groups_id',
|
||||
'r.categories_id',
|
||||
'r.size',
|
||||
'r.totalpart',
|
||||
'r.fromname',
|
||||
'r.passwordstatus',
|
||||
'r.grabs',
|
||||
'r.comments',
|
||||
'r.adddate',
|
||||
'r.videos_id',
|
||||
'r.tv_episodes_id',
|
||||
'tve.series',
|
||||
'tve.episode',
|
||||
'tve.firstaired',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, int> $categories
|
||||
* @return Builder<Release>
|
||||
*/
|
||||
private function baseReleaseQuery(int $videoId, array $categories): Builder
|
||||
{
|
||||
$query = Release::query()
|
||||
->from('releases as r')
|
||||
->where('r.videos_id', $videoId)
|
||||
->whereRaw('r.passwordstatus '.$this->releaseBrowseService->showPasswords());
|
||||
|
||||
if ($categories !== []) {
|
||||
$query->whereIn('r.categories_id', $categories);
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, int> $categories
|
||||
* @return array<int, int>
|
||||
*/
|
||||
private function fallbackSeasonCounts(int $videoId, array $categories): array
|
||||
{
|
||||
return $this->fallbackCandidates($videoId, $categories)
|
||||
->groupBy(static fn (Release $release): int => (int) $release->getAttribute('series'))
|
||||
->map(static fn (Collection $releases): int => $releases->count())
|
||||
->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, int> $categories
|
||||
* @return EloquentCollection<int, Release>
|
||||
*/
|
||||
private function fallbackReleasesForSeason(int $videoId, int $season, array $categories): EloquentCollection
|
||||
{
|
||||
return new EloquentCollection(
|
||||
$this->fallbackCandidates($videoId, $categories)
|
||||
->filter(static fn (Release $release): bool => (int) $release->getAttribute('series') === $season)
|
||||
->values()
|
||||
->all()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, int> $categories
|
||||
* @return Collection<int, Release>
|
||||
*/
|
||||
private function fallbackCandidates(int $videoId, array $categories): Collection
|
||||
{
|
||||
$candidates = $this->baseReleaseQuery($videoId, $categories)
|
||||
->where(static function (Builder $query): void {
|
||||
$query->whereNull('r.tv_episodes_id')
|
||||
->orWhere('r.tv_episodes_id', '<=', 0);
|
||||
})
|
||||
->select([
|
||||
'r.id',
|
||||
'r.searchname',
|
||||
'r.guid',
|
||||
'r.postdate',
|
||||
'r.groups_id',
|
||||
'r.categories_id',
|
||||
'r.size',
|
||||
'r.totalpart',
|
||||
'r.fromname',
|
||||
'r.passwordstatus',
|
||||
'r.grabs',
|
||||
'r.comments',
|
||||
'r.adddate',
|
||||
'r.videos_id',
|
||||
'r.tv_episodes_id',
|
||||
])
|
||||
->orderByDesc('r.postdate')
|
||||
->limit(self::FALLBACK_SCAN_LIMIT)
|
||||
->get();
|
||||
|
||||
$this->episodeHydrationService->hydrateEpisodeMetadata($candidates);
|
||||
|
||||
return $candidates
|
||||
->filter(static fn (Release $release): bool => $release->getAttribute('series') !== null && $release->getAttribute('series') !== '' && ! empty($release->getAttribute('episode')))
|
||||
->values();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, int|string> $categoryInput
|
||||
* @return array<int, int>
|
||||
*/
|
||||
public function categoryIds(array $categoryInput): array
|
||||
{
|
||||
$categories = Category::getCategorySearch($categoryInput, 'tv', true);
|
||||
|
||||
return $categories === null ? [] : array_values(array_map('intval', $categories));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
import Alpine from '@alpinejs/csp';
|
||||
|
||||
Alpine.data('seriesSeasonLoader', () => ({
|
||||
loading: false,
|
||||
_abortController: null,
|
||||
|
||||
init() {
|
||||
this.$el.addEventListener('click', event => {
|
||||
const link = event.target instanceof Element
|
||||
? event.target.closest('[data-series-season-link], [data-series-pagination-link]')
|
||||
: null;
|
||||
if (!link || !this.$el.contains(link) || link.getAttribute('href') === '#') {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
this.load(link);
|
||||
});
|
||||
|
||||
window.addEventListener('popstate', () => {
|
||||
this.loadUrl(window.location.href, false);
|
||||
});
|
||||
},
|
||||
|
||||
load(link) {
|
||||
this.loadUrl(link.href, true, link);
|
||||
},
|
||||
|
||||
loadUrl(href, pushState = true, link = null) {
|
||||
if (this.loading && this._abortController) {
|
||||
this._abortController.abort();
|
||||
}
|
||||
|
||||
const visibleUrl = new URL(href, window.location.origin);
|
||||
const requestUrl = new URL(visibleUrl.toString());
|
||||
requestUrl.hash = '';
|
||||
requestUrl.searchParams.set('_fragment', 'season');
|
||||
|
||||
this.loading = true;
|
||||
this.$el.setAttribute('aria-busy', 'true');
|
||||
this._setPanelLoading(true);
|
||||
this._abortController = new AbortController();
|
||||
|
||||
fetch(requestUrl.toString(), {
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
},
|
||||
signal: this._abortController.signal,
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`Season request failed: ${response.status}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
})
|
||||
.then(payload => {
|
||||
this._replaceHtml('[data-series-season-content]', payload.contentHtml);
|
||||
const pagination = this.$el.querySelector('[data-series-pagination-container]');
|
||||
if (pagination) {
|
||||
pagination.innerHTML = payload.paginationHtml || '';
|
||||
}
|
||||
|
||||
this._setActiveSeason(String(payload.selectedSeason));
|
||||
this._resetSelection();
|
||||
|
||||
if (pushState) {
|
||||
window.history.pushState({}, '', payload.url || visibleUrl.toString());
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
if (error.name !== 'AbortError') {
|
||||
window.location.href = href;
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false;
|
||||
this.$el.removeAttribute('aria-busy');
|
||||
this._setPanelLoading(false);
|
||||
this._abortController = null;
|
||||
});
|
||||
},
|
||||
|
||||
_replaceHtml(selector, html) {
|
||||
const current = this.$el.querySelector(selector);
|
||||
if (!current || typeof html !== 'string') {
|
||||
return;
|
||||
}
|
||||
|
||||
current.outerHTML = html;
|
||||
|
||||
const replacement = this.$el.querySelector(selector);
|
||||
if (replacement) {
|
||||
Alpine.initTree(replacement);
|
||||
}
|
||||
},
|
||||
|
||||
_setActiveSeason(season) {
|
||||
const activeClasses = ['border-blue-500', 'text-blue-600'];
|
||||
const inactiveClasses = ['border-transparent', 'text-gray-500', 'hover:text-gray-700', 'dark:text-gray-300', 'hover:border-gray-300'];
|
||||
const activeBadgeClasses = ['bg-blue-100', 'text-blue-800'];
|
||||
const inactiveBadgeClasses = ['bg-gray-100', 'dark:bg-gray-800', 'text-gray-600'];
|
||||
|
||||
this.$el.querySelectorAll('[data-series-season-link]').forEach(tab => {
|
||||
const isActive = tab.dataset.season === season;
|
||||
tab.classList.remove(...activeClasses, ...inactiveClasses);
|
||||
tab.classList.add(...(isActive ? activeClasses : inactiveClasses));
|
||||
|
||||
if (isActive) {
|
||||
tab.setAttribute('aria-current', 'page');
|
||||
} else {
|
||||
tab.removeAttribute('aria-current');
|
||||
}
|
||||
|
||||
const badge = tab.querySelector('span');
|
||||
if (badge) {
|
||||
badge.classList.remove(...activeBadgeClasses, ...inactiveBadgeClasses);
|
||||
badge.classList.add(...(isActive ? activeBadgeClasses : inactiveBadgeClasses));
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
_resetSelection() {
|
||||
this.$el.querySelectorAll('.chkRelease').forEach(checkbox => {
|
||||
checkbox.checked = false;
|
||||
});
|
||||
|
||||
const selectAll = this.$el.querySelector('#chkSelectAll');
|
||||
if (selectAll) {
|
||||
selectAll.checked = false;
|
||||
selectAll.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}
|
||||
},
|
||||
|
||||
_setPanelLoading(isLoading) {
|
||||
const content = this.$el.querySelector('[data-series-season-content]');
|
||||
if (!content) {
|
||||
return;
|
||||
}
|
||||
|
||||
content.classList.toggle('opacity-60', isLoading);
|
||||
content.classList.toggle('pointer-events-none', isLoading);
|
||||
},
|
||||
}));
|
||||
@@ -0,0 +1,76 @@
|
||||
<div class="p-4" data-series-season-content>
|
||||
@forelse($seasons as $seasonNumber => $episodes)
|
||||
<div class="season-content" data-season="{{ $seasonNumber }}">
|
||||
@foreach($episodes as $episodeNumber => $releases)
|
||||
<div class="mb-4 pb-4 border-b border-gray-200 dark:border-gray-700 last:border-b-0">
|
||||
<h6 class="font-semibold text-gray-700 dark:text-gray-300 mb-2">
|
||||
Episode {{ $episodeNumber }}
|
||||
</h6>
|
||||
<div class="space-y-2">
|
||||
@foreach($releases as $release)
|
||||
<div class="series-episode-card flex items-center gap-3 bg-gray-50 dark:bg-gray-900 rounded-lg p-3 hover:bg-gray-100 dark:hover:bg-gray-800">
|
||||
<div class="shrink-0">
|
||||
<input type="checkbox" class="chkRelease rounded border-gray-300 dark:border-gray-600 text-blue-600 dark:text-blue-500 focus:ring-blue-500 dark:focus:ring-blue-400 dark:bg-gray-700" name="release[]" value="{{ $release->guid }}" @change="onCheckboxChange()">
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center gap-2 flex-wrap">
|
||||
<a href="{{ url('/details/' . $release->guid) }}"
|
||||
class="text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 font-medium wrap-break-word break-all">
|
||||
{{ $release->searchname }}
|
||||
</a>
|
||||
@if(($release->failed_count ?? 0) > 0)
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-red-100 text-red-800"
|
||||
title="{{ $release->failed_count }} user(s) reported download failure">
|
||||
<i class="fas fa-exclamation-triangle mr-1"></i> Failed ({{ $release->failed_count }})
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
<div class="text-xs text-gray-500 mt-1 flex flex-wrap gap-2">
|
||||
<span class="mr-3">
|
||||
<i class="fa fa-hdd-o mr-1"></i>{{ formatBytes($release->size) }}
|
||||
</span>
|
||||
<span>
|
||||
<i class="fa fa-clock-o mr-1"></i>Added: {{ userDateDiffForHumans($release->adddate) }}
|
||||
</span>
|
||||
@if(!empty($release->postdate))
|
||||
<span>
|
||||
<i class="fas fa-calendar mr-1"></i> Posted: {{ userDate($release->postdate, 'M d, Y H:i') }}
|
||||
</span>
|
||||
@endif
|
||||
@if(!empty($release->fromname))
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded bg-indigo-100 dark:bg-indigo-900 text-indigo-800 dark:text-indigo-200 font-mono">
|
||||
<i class="fas fa-user mr-1"></i>{{ $release->fromname }}
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<a href="{{ url('/getnzb?id=' . $release->guid) }}"
|
||||
class="download-nzb px-3 py-1 bg-green-600 dark:bg-green-700 text-white rounded hover:bg-green-700 dark:hover:bg-green-800 text-sm"
|
||||
title="Download NZB">
|
||||
<i class="fa fa-download"></i>
|
||||
</a>
|
||||
<a href="{{ url('/details/' . $release->guid) }}"
|
||||
class="px-3 py-1 bg-blue-600 dark:bg-blue-700 text-white rounded hover:bg-blue-700 dark:hover:bg-blue-800 text-sm"
|
||||
title="View Details">
|
||||
<i class="fa fa-info-circle"></i>
|
||||
</a>
|
||||
<a href="#" class="add-to-cart px-3 py-1 bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded hover:bg-gray-300 text-sm"
|
||||
data-guid="{{ $release->guid }}"
|
||||
title="Add to cart">
|
||||
<i class="icon_cart fa fa-shopping-basket"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@empty
|
||||
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4 text-blue-800">
|
||||
<i class="fa fa-info-circle mr-2"></i>
|
||||
No releases found on this page for the selected season.
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
@@ -0,0 +1,29 @@
|
||||
@if(!empty($pagination['per_page']) && $pagination['per_page'] > 0 && ($pagination['total_pages'] ?? 1) > 1)
|
||||
<div class="series-stat-card mt-6 flex items-center justify-between bg-gray-50 dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-lg p-4" data-series-pagination>
|
||||
<div class="text-sm text-gray-600 dark:text-gray-300">
|
||||
Page {{ $pagination['current_page'] }} of {{ $pagination['total_pages'] }}
|
||||
<span class="ml-2 text-xs text-gray-500">({{ number_format($pagination['total_rows']) }} total releases)</span>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
@php
|
||||
$prevPage = max($pagination['current_page'] - 1, 1);
|
||||
$nextPage = min($pagination['current_page'] + 1, $pagination['total_pages']);
|
||||
$paginationQuery = request()->query();
|
||||
unset($paginationQuery['_fragment']);
|
||||
$paginationBaseUrl = url()->current();
|
||||
$prevUrl = $pagination['current_page'] > 1 ? $paginationBaseUrl . '?' . http_build_query(array_merge($paginationQuery, ['page' => $prevPage])) . '#series-episodes' : '#';
|
||||
$nextUrl = $pagination['current_page'] < $pagination['total_pages'] ? $paginationBaseUrl . '?' . http_build_query(array_merge($paginationQuery, ['page' => $nextPage])) . '#series-episodes' : '#';
|
||||
@endphp
|
||||
<a href="{{ $prevUrl }}"
|
||||
@if($pagination['current_page'] > 1) data-series-pagination-link @endif
|
||||
class="px-4 py-2 rounded-lg text-sm font-medium {{ $pagination['current_page'] > 1 ? 'bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700' : 'bg-gray-200 dark:bg-gray-700 text-gray-400 cursor-not-allowed' }}">
|
||||
<i class="fas fa-arrow-left mr-2"></i>Previous
|
||||
</a>
|
||||
<a href="{{ $nextUrl }}"
|
||||
@if($pagination['current_page'] < $pagination['total_pages']) data-series-pagination-link @endif
|
||||
class="px-4 py-2 rounded-lg text-sm font-medium {{ $pagination['current_page'] < $pagination['total_pages'] ? 'bg-blue-600 text-white hover:bg-blue-700' : 'bg-gray-200 dark:bg-gray-700 text-gray-400 cursor-not-allowed' }}">
|
||||
Next<i class="fas fa-arrow-right ml-2"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
@@ -0,0 +1,496 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Http\Middleware\ClearanceMiddleware;
|
||||
use App\Http\Middleware\Google2FAMiddleware;
|
||||
use App\Http\Middleware\TrustedDevice2FAMiddleware;
|
||||
use App\Models\Category;
|
||||
use App\Models\User;
|
||||
use App\View\Composers\GlobalDataComposer;
|
||||
use Illuminate\Contracts\Console\Kernel;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use PDO;
|
||||
use ReflectionClass;
|
||||
use Spatie\Permission\PermissionRegistrar;
|
||||
use Tests\TestCase;
|
||||
|
||||
class SeriesControllerTest extends TestCase
|
||||
{
|
||||
private string $databasePath;
|
||||
|
||||
/**
|
||||
* @var array<string, string|false>
|
||||
*/
|
||||
private array $originalEnvironment = [];
|
||||
|
||||
public function createApplication()
|
||||
{
|
||||
$this->databasePath = sys_get_temp_dir().'/nntmux-series-controller-test.sqlite';
|
||||
|
||||
$this->originalEnvironment = [
|
||||
'APP_ENV' => getenv('APP_ENV'),
|
||||
'DB_CONNECTION' => getenv('DB_CONNECTION'),
|
||||
'DB_DATABASE' => getenv('DB_DATABASE'),
|
||||
];
|
||||
|
||||
if (file_exists($this->databasePath)) {
|
||||
unlink($this->databasePath);
|
||||
}
|
||||
|
||||
$pdo = new PDO('sqlite:'.$this->databasePath);
|
||||
$pdo->exec('CREATE TABLE settings (name VARCHAR PRIMARY KEY, value TEXT NULL)');
|
||||
$pdo->exec("INSERT INTO settings (name, value) VALUES
|
||||
('showpasswordedrelease', '0'),
|
||||
('categorizeforeign', '0'),
|
||||
('catwebdl', '0'),
|
||||
('title', 'NNTmux Test'),
|
||||
('home_link', '/')");
|
||||
|
||||
$this->setEnvironmentValue('APP_ENV', 'testing');
|
||||
$this->setEnvironmentValue('DB_CONNECTION', 'sqlite');
|
||||
$this->setEnvironmentValue('DB_DATABASE', $this->databasePath);
|
||||
|
||||
$app = require __DIR__.'/../../bootstrap/app.php';
|
||||
$app->make(Kernel::class)->bootstrap();
|
||||
|
||||
return $app;
|
||||
}
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
config([
|
||||
'database.default' => 'sqlite',
|
||||
'database.connections.sqlite.database' => $this->databasePath,
|
||||
'mail.from.address' => 'noreply@example.test',
|
||||
'mail.from.name' => 'NNTmux Tests',
|
||||
'app.key' => 'base64:'.base64_encode(random_bytes(32)),
|
||||
'nntmux.series_view_limit' => 20,
|
||||
]);
|
||||
|
||||
DB::purge();
|
||||
DB::reconnect();
|
||||
Cache::flush();
|
||||
|
||||
$this->createSchema();
|
||||
$this->seedBaseData();
|
||||
$this->resetGlobalComposerState();
|
||||
app(PermissionRegistrar::class)->forgetCachedPermissions();
|
||||
$this->withoutMiddleware([
|
||||
ClearanceMiddleware::class,
|
||||
Google2FAMiddleware::class,
|
||||
TrustedDevice2FAMiddleware::class,
|
||||
]);
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
if ($this->databasePath !== '' && file_exists($this->databasePath)) {
|
||||
unlink($this->databasePath);
|
||||
}
|
||||
|
||||
parent::tearDown();
|
||||
|
||||
foreach ($this->originalEnvironment as $key => $value) {
|
||||
$this->setEnvironmentValue($key, $value === false ? null : $value);
|
||||
}
|
||||
}
|
||||
|
||||
public function test_selected_season_renders_only_that_season_releases(): void
|
||||
{
|
||||
$user = $this->createUser();
|
||||
$videoId = $this->createShow();
|
||||
$this->createMatchedRelease($videoId, 1, 1, 'Only.Season.One.S01E01.720p-GROUP');
|
||||
$this->createMatchedRelease($videoId, 2, 1, 'Only.Season.Two.S02E01.720p-GROUP');
|
||||
|
||||
$seasonOne = $this->actingAs($user)->get(route('series', ['id' => $videoId, 'season' => 1]));
|
||||
$seasonOne->assertOk();
|
||||
$seasonOne->assertSee('Only.Season.One.S01E01.720p-GROUP');
|
||||
$seasonOne->assertDontSee('Only.Season.Two.S02E01.720p-GROUP');
|
||||
|
||||
$seasonTwo = $this->actingAs($user)->get(route('series', ['id' => $videoId, 'season' => 2]));
|
||||
$seasonTwo->assertOk();
|
||||
$seasonTwo->assertSee('Only.Season.Two.S02E01.720p-GROUP');
|
||||
$seasonTwo->assertDontSee('Only.Season.One.S01E01.720p-GROUP');
|
||||
}
|
||||
|
||||
public function test_season_links_preserve_category_and_reset_page(): void
|
||||
{
|
||||
$user = $this->createUser();
|
||||
$videoId = $this->createShow();
|
||||
$this->createMatchedRelease($videoId, 1, 1, 'Link.Test.S01E01.720p-GROUP');
|
||||
$this->createMatchedRelease($videoId, 2, 1, 'Link.Test.S02E01.720p-GROUP');
|
||||
|
||||
$response = $this->actingAs($user)->get(route('series', [
|
||||
'id' => $videoId,
|
||||
'season' => 1,
|
||||
'page' => 3,
|
||||
't' => Category::TV_SD,
|
||||
]));
|
||||
|
||||
$response->assertOk();
|
||||
$html = $response->getContent();
|
||||
|
||||
$this->assertStringContainsString('season=2', $html);
|
||||
$this->assertStringContainsString('page=1', $html);
|
||||
$this->assertStringContainsString('t=5030', $html);
|
||||
$this->assertStringContainsString('#series-episodes', $html);
|
||||
$this->assertStringContainsString('x-data="seriesSeasonLoader"', $html);
|
||||
$this->assertStringContainsString('data-series-season-link', $html);
|
||||
$this->assertStringNotContainsString('season=2&page=3', $html);
|
||||
}
|
||||
|
||||
public function test_lazy_season_fragment_returns_only_selected_season_html(): void
|
||||
{
|
||||
$user = $this->createUser();
|
||||
$videoId = $this->createShow();
|
||||
$this->createMatchedRelease($videoId, 1, 1, 'Lazy.Test.S01E01.720p-GROUP');
|
||||
$this->createMatchedRelease($videoId, 2, 1, 'Lazy.Test.S02E01.720p-GROUP');
|
||||
|
||||
$response = $this->actingAs($user)->getJson(route('series', [
|
||||
'id' => $videoId,
|
||||
'season' => 2,
|
||||
'_fragment' => 'season',
|
||||
]));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJsonPath('selectedSeason', 2);
|
||||
|
||||
$contentHtml = $response->json('contentHtml');
|
||||
$this->assertIsString($contentHtml);
|
||||
$this->assertStringContainsString('data-series-season-content', $contentHtml);
|
||||
$this->assertStringContainsString('Lazy.Test.S02E01.720p-GROUP', $contentHtml);
|
||||
$this->assertStringNotContainsString('Lazy.Test.S01E01.720p-GROUP', $contentHtml);
|
||||
$this->assertStringNotContainsString('<!DOCTYPE html>', $contentHtml);
|
||||
$this->assertStringNotContainsString('_fragment=season', (string) $response->json('url'));
|
||||
}
|
||||
|
||||
public function test_many_releases_only_render_selected_season_page(): void
|
||||
{
|
||||
$user = $this->createUser();
|
||||
$videoId = $this->createShow();
|
||||
|
||||
for ($episode = 1; $episode <= 30; $episode++) {
|
||||
$this->createMatchedRelease($videoId, 1, $episode, sprintf('Paged.Show.S01E%02d.720p-GROUP', $episode));
|
||||
$this->createMatchedRelease($videoId, 2, $episode, sprintf('Paged.Show.S02E%02d.720p-GROUP', $episode));
|
||||
}
|
||||
|
||||
$response = $this->actingAs($user)->get(route('series', ['id' => $videoId, 'season' => 1]));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertSee('Paged.Show.S01E01.720p-GROUP');
|
||||
$response->assertDontSee('Paged.Show.S02E01.720p-GROUP');
|
||||
$this->assertSame(20, substr_count($response->getContent(), 'series-episode-card'));
|
||||
}
|
||||
|
||||
private function createSchema(): void
|
||||
{
|
||||
if (! Schema::hasTable('settings')) {
|
||||
Schema::create('settings', function (Blueprint $table): void {
|
||||
$table->string('name')->primary();
|
||||
$table->text('value')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
Schema::create('roles', function (Blueprint $table): void {
|
||||
$table->increments('id');
|
||||
$table->string('name');
|
||||
$table->string('guard_name')->default('web');
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
Schema::create('permissions', function (Blueprint $table): void {
|
||||
$table->increments('id');
|
||||
$table->string('name');
|
||||
$table->string('guard_name')->default('web');
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
Schema::create('model_has_roles', function (Blueprint $table): void {
|
||||
$table->unsignedInteger('role_id');
|
||||
$table->string('model_type');
|
||||
$table->unsignedInteger('model_id');
|
||||
$table->primary(['role_id', 'model_id', 'model_type']);
|
||||
});
|
||||
|
||||
Schema::create('model_has_permissions', function (Blueprint $table): void {
|
||||
$table->unsignedInteger('permission_id');
|
||||
$table->string('model_type');
|
||||
$table->unsignedInteger('model_id');
|
||||
$table->primary(['permission_id', 'model_id', 'model_type']);
|
||||
});
|
||||
|
||||
Schema::create('role_has_permissions', function (Blueprint $table): void {
|
||||
$table->unsignedInteger('permission_id');
|
||||
$table->unsignedInteger('role_id');
|
||||
$table->primary(['permission_id', 'role_id']);
|
||||
});
|
||||
|
||||
Schema::create('users', function (Blueprint $table): void {
|
||||
$table->increments('id');
|
||||
$table->string('username');
|
||||
$table->string('email')->unique();
|
||||
$table->string('password');
|
||||
$table->unsignedInteger('roles_id')->default(1);
|
||||
$table->integer('rate_limit')->default(60);
|
||||
$table->string('api_token')->nullable();
|
||||
$table->boolean('verified')->default(true);
|
||||
$table->boolean('can_post')->default(true);
|
||||
$table->string('theme_preference', 10)->default('light');
|
||||
$table->string('session_token')->nullable();
|
||||
$table->timestamp('email_verified_at')->nullable();
|
||||
$table->timestamp('lastlogin')->nullable();
|
||||
$table->rememberToken();
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
});
|
||||
|
||||
Schema::create('user_excluded_categories', function (Blueprint $table): void {
|
||||
$table->increments('id');
|
||||
$table->unsignedInteger('users_id');
|
||||
$table->unsignedInteger('categories_id');
|
||||
});
|
||||
|
||||
Schema::create('content', function (Blueprint $table): void {
|
||||
$table->increments('id');
|
||||
$table->string('title')->default('');
|
||||
$table->string('url')->nullable();
|
||||
$table->text('body')->nullable();
|
||||
$table->text('metadescription')->nullable();
|
||||
$table->text('metakeywords')->nullable();
|
||||
$table->integer('contenttype')->default(1);
|
||||
$table->integer('status')->default(1);
|
||||
$table->integer('ordinal')->nullable();
|
||||
$table->integer('role')->default(0);
|
||||
});
|
||||
|
||||
Schema::create('root_categories', function (Blueprint $table): void {
|
||||
$table->increments('id');
|
||||
$table->string('title')->default('');
|
||||
$table->integer('status')->default(1);
|
||||
});
|
||||
|
||||
Schema::create('categories', function (Blueprint $table): void {
|
||||
$table->increments('id');
|
||||
$table->string('title')->default('');
|
||||
$table->unsignedInteger('root_categories_id')->nullable();
|
||||
$table->integer('status')->default(1);
|
||||
$table->text('description')->nullable();
|
||||
});
|
||||
|
||||
Schema::create('videos', function (Blueprint $table): void {
|
||||
$table->increments('id');
|
||||
$table->integer('type')->default(0);
|
||||
$table->string('title')->default('');
|
||||
$table->string('countries_id', 2)->nullable();
|
||||
$table->string('started')->nullable();
|
||||
$table->integer('anidb')->default(0);
|
||||
$table->string('imdb')->nullable();
|
||||
$table->integer('tmdb')->default(0);
|
||||
$table->integer('trakt')->default(0);
|
||||
$table->integer('tvdb')->default(0);
|
||||
$table->integer('tvmaze')->default(0);
|
||||
$table->integer('tvrage')->default(0);
|
||||
$table->integer('source')->default(0);
|
||||
});
|
||||
|
||||
Schema::create('tv_info', function (Blueprint $table): void {
|
||||
$table->unsignedInteger('videos_id')->primary();
|
||||
$table->text('summary')->nullable();
|
||||
$table->string('publisher')->nullable();
|
||||
$table->boolean('image')->default(false);
|
||||
});
|
||||
|
||||
Schema::create('tv_episodes', function (Blueprint $table): void {
|
||||
$table->increments('id');
|
||||
$table->unsignedInteger('videos_id');
|
||||
$table->integer('series')->default(0);
|
||||
$table->integer('episode')->default(0);
|
||||
$table->string('se_complete')->default('');
|
||||
$table->string('title')->default('');
|
||||
$table->string('firstaired')->nullable();
|
||||
$table->text('summary')->nullable();
|
||||
});
|
||||
|
||||
Schema::create('releases', function (Blueprint $table): void {
|
||||
$table->increments('id');
|
||||
$table->string('name')->nullable();
|
||||
$table->string('searchname')->default('');
|
||||
$table->string('fromname')->nullable();
|
||||
$table->dateTime('postdate')->nullable();
|
||||
$table->dateTime('adddate')->nullable();
|
||||
$table->string('guid')->nullable();
|
||||
$table->unsignedInteger('categories_id')->default(Category::TV_SD);
|
||||
$table->unsignedInteger('groups_id')->nullable();
|
||||
$table->unsignedBigInteger('size')->default(0);
|
||||
$table->integer('totalpart')->default(0);
|
||||
$table->integer('passwordstatus')->default(0);
|
||||
$table->integer('grabs')->default(0);
|
||||
$table->integer('comments')->default(0);
|
||||
$table->unsignedInteger('videos_id')->nullable();
|
||||
$table->integer('tv_episodes_id')->nullable();
|
||||
});
|
||||
|
||||
Schema::create('dnzb_failures', function (Blueprint $table): void {
|
||||
$table->unsignedInteger('release_id');
|
||||
$table->unsignedInteger('users_id');
|
||||
$table->integer('failed')->default(0);
|
||||
$table->primary(['release_id', 'users_id']);
|
||||
});
|
||||
|
||||
Schema::create('user_series', function (Blueprint $table): void {
|
||||
$table->increments('id');
|
||||
$table->unsignedInteger('users_id');
|
||||
$table->unsignedInteger('videos_id');
|
||||
});
|
||||
}
|
||||
|
||||
private function seedBaseData(): void
|
||||
{
|
||||
DB::table('root_categories')->insert([
|
||||
'id' => Category::TV_ROOT,
|
||||
'title' => 'TV',
|
||||
'status' => 1,
|
||||
]);
|
||||
|
||||
DB::table('categories')->insert([
|
||||
'id' => Category::TV_SD,
|
||||
'title' => 'SD',
|
||||
'root_categories_id' => Category::TV_ROOT,
|
||||
'status' => 1,
|
||||
'description' => 'TV SD',
|
||||
]);
|
||||
|
||||
DB::table('roles')->insert([
|
||||
'id' => 1,
|
||||
'name' => 'User',
|
||||
'guard_name' => 'web',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
DB::table('permissions')->insert([
|
||||
'id' => 1,
|
||||
'name' => 'view tv',
|
||||
'guard_name' => 'web',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
DB::table('role_has_permissions')->insert([
|
||||
'permission_id' => 1,
|
||||
'role_id' => 1,
|
||||
]);
|
||||
}
|
||||
|
||||
private function createUser(): User
|
||||
{
|
||||
$userId = DB::table('users')->insertGetId([
|
||||
'username' => 'series-user',
|
||||
'email' => 'series@example.test',
|
||||
'password' => bcrypt('secret'),
|
||||
'roles_id' => 1,
|
||||
'api_token' => 'series-token',
|
||||
'verified' => true,
|
||||
'can_post' => true,
|
||||
'theme_preference' => 'light',
|
||||
'email_verified_at' => now(),
|
||||
'lastlogin' => now(),
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$userModelClass = User::class;
|
||||
DB::table('model_has_roles')->insert([
|
||||
'role_id' => 1,
|
||||
'model_type' => $userModelClass,
|
||||
'model_id' => $userId,
|
||||
]);
|
||||
DB::table('model_has_permissions')->insert([
|
||||
'permission_id' => 1,
|
||||
'model_type' => $userModelClass,
|
||||
'model_id' => $userId,
|
||||
]);
|
||||
|
||||
return User::query()->findOrFail($userId);
|
||||
}
|
||||
|
||||
private function createShow(): int
|
||||
{
|
||||
$videoId = DB::table('videos')->insertGetId([
|
||||
'type' => 0,
|
||||
'title' => 'Paged Test Show',
|
||||
'started' => '2024-01-01',
|
||||
'countries_id' => 'US',
|
||||
]);
|
||||
|
||||
DB::table('tv_info')->insert([
|
||||
'videos_id' => $videoId,
|
||||
'summary' => 'A test show.',
|
||||
'publisher' => 'Test Network',
|
||||
'image' => 0,
|
||||
]);
|
||||
|
||||
return (int) $videoId;
|
||||
}
|
||||
|
||||
private function createMatchedRelease(int $videoId, int $season, int $episode, string $searchName): void
|
||||
{
|
||||
$episodeId = DB::table('tv_episodes')->insertGetId([
|
||||
'videos_id' => $videoId,
|
||||
'series' => $season,
|
||||
'episode' => $episode,
|
||||
'se_complete' => sprintf('S%02dE%02d', $season, $episode),
|
||||
'title' => 'Episode '.$episode,
|
||||
'firstaired' => now()->subDays($episode)->toDateString(),
|
||||
'summary' => 'Episode summary.',
|
||||
]);
|
||||
|
||||
DB::table('releases')->insert([
|
||||
'name' => $searchName,
|
||||
'searchname' => $searchName,
|
||||
'fromname' => 'poster@example.test',
|
||||
'postdate' => now()->subMinutes($episode),
|
||||
'adddate' => now()->subMinutes($episode),
|
||||
'guid' => sha1($searchName),
|
||||
'categories_id' => Category::TV_SD,
|
||||
'groups_id' => null,
|
||||
'size' => 1024,
|
||||
'totalpart' => 1,
|
||||
'passwordstatus' => 0,
|
||||
'grabs' => 0,
|
||||
'comments' => 0,
|
||||
'videos_id' => $videoId,
|
||||
'tv_episodes_id' => $episodeId,
|
||||
]);
|
||||
}
|
||||
|
||||
private function resetGlobalComposerState(): void
|
||||
{
|
||||
$reflection = new ReflectionClass(GlobalDataComposer::class);
|
||||
$property = $reflection->getProperty('resolvedData');
|
||||
$property->setAccessible(true);
|
||||
$property->setValue(null, null);
|
||||
}
|
||||
|
||||
private function setEnvironmentValue(string $key, ?string $value): void
|
||||
{
|
||||
if ($value === null) {
|
||||
putenv($key);
|
||||
unset($_ENV[$key], $_SERVER[$key]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
putenv($key.'='.$value);
|
||||
$_ENV[$key] = $value;
|
||||
$_SERVER[$key] = $value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use App\Models\Category;
|
||||
use App\Services\SeriesReleaseService;
|
||||
use Illuminate\Contracts\Console\Kernel;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use PDO;
|
||||
use Tests\TestCase;
|
||||
|
||||
class SeriesReleaseServiceTest extends TestCase
|
||||
{
|
||||
private string $databasePath;
|
||||
|
||||
/**
|
||||
* @var array<string, string|false>
|
||||
*/
|
||||
private array $originalEnvironment = [];
|
||||
|
||||
public function createApplication()
|
||||
{
|
||||
$this->databasePath = sys_get_temp_dir().'/nntmux-series-release-service-test.sqlite';
|
||||
|
||||
$this->originalEnvironment = [
|
||||
'APP_ENV' => getenv('APP_ENV'),
|
||||
'DB_CONNECTION' => getenv('DB_CONNECTION'),
|
||||
'DB_DATABASE' => getenv('DB_DATABASE'),
|
||||
];
|
||||
|
||||
if (file_exists($this->databasePath)) {
|
||||
unlink($this->databasePath);
|
||||
}
|
||||
|
||||
$pdo = new PDO('sqlite:'.$this->databasePath);
|
||||
$pdo->exec('CREATE TABLE settings (name VARCHAR PRIMARY KEY, value TEXT NULL)');
|
||||
$pdo->exec("INSERT INTO settings (name, value) VALUES ('showpasswordedrelease', '0'), ('categorizeforeign', '0'), ('catwebdl', '0')");
|
||||
|
||||
$this->setEnvironmentValue('APP_ENV', 'testing');
|
||||
$this->setEnvironmentValue('DB_CONNECTION', 'sqlite');
|
||||
$this->setEnvironmentValue('DB_DATABASE', $this->databasePath);
|
||||
|
||||
$app = require __DIR__.'/../../bootstrap/app.php';
|
||||
$app->make(Kernel::class)->bootstrap();
|
||||
|
||||
return $app;
|
||||
}
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
config([
|
||||
'database.default' => 'sqlite',
|
||||
'database.connections.sqlite.database' => $this->databasePath,
|
||||
]);
|
||||
|
||||
DB::purge();
|
||||
DB::reconnect();
|
||||
Cache::flush();
|
||||
|
||||
$this->createSchema();
|
||||
$this->seedCategories();
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
if ($this->databasePath !== '' && file_exists($this->databasePath)) {
|
||||
unlink($this->databasePath);
|
||||
}
|
||||
|
||||
parent::tearDown();
|
||||
|
||||
foreach ($this->originalEnvironment as $key => $value) {
|
||||
$this->setEnvironmentValue($key, $value === false ? null : $value);
|
||||
}
|
||||
}
|
||||
|
||||
public function test_fallback_releases_are_only_in_their_parsed_season(): void
|
||||
{
|
||||
$videoId = $this->createShow();
|
||||
|
||||
$this->createRelease($videoId, 'Test.Show.S01E01.720p-GROUP', null);
|
||||
$this->createRelease($videoId, 'Test.Show.S02E03.720p-GROUP', null);
|
||||
|
||||
/** @var SeriesReleaseService $service */
|
||||
$service = app(SeriesReleaseService::class);
|
||||
$categories = $service->categoryIds([-1]);
|
||||
|
||||
$seasonOne = $service->releasesForSeason($videoId, 1, 0, 20, $categories);
|
||||
$seasonTwo = $service->releasesForSeason($videoId, 2, 0, 20, $categories);
|
||||
|
||||
$this->assertSame(['Test.Show.S01E01.720p-GROUP'], $seasonOne['releases']->pluck('searchname')->all());
|
||||
$this->assertSame(['Test.Show.S02E03.720p-GROUP'], $seasonTwo['releases']->pluck('searchname')->all());
|
||||
}
|
||||
|
||||
private function createSchema(): void
|
||||
{
|
||||
if (! Schema::hasTable('settings')) {
|
||||
Schema::create('settings', function (Blueprint $table): void {
|
||||
$table->string('name')->primary();
|
||||
$table->text('value')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
Schema::create('root_categories', function (Blueprint $table): void {
|
||||
$table->increments('id');
|
||||
$table->string('title')->default('');
|
||||
$table->integer('status')->default(1);
|
||||
});
|
||||
|
||||
Schema::create('categories', function (Blueprint $table): void {
|
||||
$table->increments('id');
|
||||
$table->string('title')->default('');
|
||||
$table->unsignedInteger('root_categories_id')->nullable();
|
||||
$table->integer('status')->default(1);
|
||||
$table->text('description')->nullable();
|
||||
});
|
||||
|
||||
Schema::create('videos', function (Blueprint $table): void {
|
||||
$table->increments('id');
|
||||
$table->integer('type')->default(0);
|
||||
$table->string('title')->default('');
|
||||
$table->string('countries_id', 2)->nullable();
|
||||
$table->string('started')->nullable();
|
||||
$table->integer('anidb')->default(0);
|
||||
$table->string('imdb')->nullable();
|
||||
$table->integer('tmdb')->default(0);
|
||||
$table->integer('trakt')->default(0);
|
||||
$table->integer('tvdb')->default(0);
|
||||
$table->integer('tvmaze')->default(0);
|
||||
$table->integer('tvrage')->default(0);
|
||||
$table->integer('source')->default(0);
|
||||
});
|
||||
|
||||
Schema::create('tv_info', function (Blueprint $table): void {
|
||||
$table->unsignedInteger('videos_id')->primary();
|
||||
$table->text('summary')->nullable();
|
||||
$table->string('publisher')->nullable();
|
||||
$table->boolean('image')->default(false);
|
||||
});
|
||||
|
||||
Schema::create('tv_episodes', function (Blueprint $table): void {
|
||||
$table->increments('id');
|
||||
$table->unsignedInteger('videos_id');
|
||||
$table->integer('series')->default(0);
|
||||
$table->integer('episode')->default(0);
|
||||
$table->string('se_complete')->default('');
|
||||
$table->string('title')->default('');
|
||||
$table->string('firstaired')->nullable();
|
||||
$table->text('summary')->nullable();
|
||||
});
|
||||
|
||||
Schema::create('releases', function (Blueprint $table): void {
|
||||
$table->increments('id');
|
||||
$table->string('name')->nullable();
|
||||
$table->string('searchname')->default('');
|
||||
$table->string('fromname')->nullable();
|
||||
$table->dateTime('postdate')->nullable();
|
||||
$table->dateTime('adddate')->nullable();
|
||||
$table->string('guid')->nullable();
|
||||
$table->unsignedInteger('categories_id')->default(Category::TV_SD);
|
||||
$table->unsignedInteger('groups_id')->nullable();
|
||||
$table->unsignedBigInteger('size')->default(0);
|
||||
$table->integer('totalpart')->default(0);
|
||||
$table->integer('passwordstatus')->default(0);
|
||||
$table->integer('grabs')->default(0);
|
||||
$table->integer('comments')->default(0);
|
||||
$table->unsignedInteger('videos_id')->nullable();
|
||||
$table->integer('tv_episodes_id')->nullable();
|
||||
});
|
||||
|
||||
Schema::create('dnzb_failures', function (Blueprint $table): void {
|
||||
$table->unsignedInteger('release_id');
|
||||
$table->unsignedInteger('users_id');
|
||||
$table->integer('failed')->default(0);
|
||||
$table->primary(['release_id', 'users_id']);
|
||||
});
|
||||
}
|
||||
|
||||
private function seedCategories(): void
|
||||
{
|
||||
DB::table('root_categories')->insert([
|
||||
'id' => Category::TV_ROOT,
|
||||
'title' => 'TV',
|
||||
'status' => 1,
|
||||
]);
|
||||
|
||||
DB::table('categories')->insert([
|
||||
'id' => Category::TV_SD,
|
||||
'title' => 'SD',
|
||||
'root_categories_id' => Category::TV_ROOT,
|
||||
'status' => 1,
|
||||
'description' => 'TV SD',
|
||||
]);
|
||||
}
|
||||
|
||||
private function createShow(): int
|
||||
{
|
||||
$videoId = DB::table('videos')->insertGetId([
|
||||
'type' => 0,
|
||||
'title' => 'Test Show',
|
||||
'started' => '2024-01-01',
|
||||
]);
|
||||
|
||||
DB::table('tv_info')->insert([
|
||||
'videos_id' => $videoId,
|
||||
'summary' => 'A test show.',
|
||||
'publisher' => 'Test Network',
|
||||
'image' => 0,
|
||||
]);
|
||||
|
||||
return (int) $videoId;
|
||||
}
|
||||
|
||||
private function createRelease(int $videoId, string $searchName, ?int $episodeId): void
|
||||
{
|
||||
DB::table('releases')->insert([
|
||||
'name' => $searchName,
|
||||
'searchname' => $searchName,
|
||||
'fromname' => 'poster@example.test',
|
||||
'postdate' => now(),
|
||||
'adddate' => now(),
|
||||
'guid' => sha1($searchName),
|
||||
'categories_id' => Category::TV_SD,
|
||||
'groups_id' => null,
|
||||
'size' => 1024,
|
||||
'totalpart' => 1,
|
||||
'passwordstatus' => 0,
|
||||
'grabs' => 0,
|
||||
'comments' => 0,
|
||||
'videos_id' => $videoId,
|
||||
'tv_episodes_id' => $episodeId,
|
||||
]);
|
||||
}
|
||||
|
||||
private function setEnvironmentValue(string $key, ?string $value): void
|
||||
{
|
||||
if ($value === null) {
|
||||
putenv($key);
|
||||
unset($_ENV[$key], $_SERVER[$key]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
putenv($key.'='.$value);
|
||||
$_ENV[$key] = $value;
|
||||
$_SERVER[$key] = $value;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user