Compare commits
15 Commits
21f61b0911
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
301244cdb0 | ||
|
|
018597faaa | ||
|
|
664d3b4d5b | ||
|
|
9d2b5414fa | ||
|
|
cfc84a1d4d | ||
|
|
a15c6a1a44 | ||
|
|
32ead90121 | ||
|
|
dce3cdd929 | ||
|
|
5784cf8ed3 | ||
|
|
5902bf7c2b | ||
|
|
d4d8330d83 | ||
|
|
25e28f8193 | ||
|
|
b992d05312 | ||
|
|
55c104629f | ||
|
|
2c62880282 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -14,3 +14,4 @@ npm-debug.log
|
|||||||
yarn-error.log
|
yarn-error.log
|
||||||
/.idea
|
/.idea
|
||||||
/.vscode
|
/.vscode
|
||||||
|
/public/videos
|
||||||
|
|||||||
76
README.md
76
README.md
@@ -1,64 +1,32 @@
|
|||||||
<p align="center"><a href="https://laravel.com" target="_blank"><img src="https://raw.githubusercontent.com/laravel/art/master/logo-lockup/5%20SVG/2%20CMYK/1%20Full%20Color/laravel-logolockup-cmyk-red.svg" width="400"></a></p>
|
|
||||||
|
|
||||||
<p align="center">
|
## Commands
|
||||||
<a href="https://travis-ci.org/laravel/framework"><img src="https://travis-ci.org/laravel/framework.svg" alt="Build Status"></a>
|
|
||||||
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/dt/laravel/framework" alt="Total Downloads"></a>
|
|
||||||
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/v/laravel/framework" alt="Latest Stable Version"></a>
|
|
||||||
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/l/laravel/framework" alt="License"></a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
## About Laravel
|
Updates information about courses and chapters
|
||||||
|
|
||||||
Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as:
|
```php
|
||||||
|
php artisan symfonycast:update
|
||||||
|
```
|
||||||
|
|
||||||
- [Simple, fast routing engine](https://laravel.com/docs/routing).
|
Dispatch chapters marked as sync offline to queue for download
|
||||||
- [Powerful dependency injection container](https://laravel.com/docs/container).
|
|
||||||
- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage.
|
|
||||||
- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent).
|
|
||||||
- Database agnostic [schema migrations](https://laravel.com/docs/migrations).
|
|
||||||
- [Robust background job processing](https://laravel.com/docs/queues).
|
|
||||||
- [Real-time event broadcasting](https://laravel.com/docs/broadcasting).
|
|
||||||
|
|
||||||
Laravel is accessible, powerful, and provides tools required for large, robust applications.
|
```php
|
||||||
|
php artisan symfonycast:sync
|
||||||
|
```
|
||||||
|
Dispatch chapters without file size to queue for update
|
||||||
|
|
||||||
## Learning Laravel
|
```php
|
||||||
|
php artisan symfonycast:video_size
|
||||||
|
```
|
||||||
|
|
||||||
Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework.
|
## Queues
|
||||||
|
|
||||||
If you don't feel like reading, [Laracasts](https://laracasts.com) can help. Laracasts contains over 2000 video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library.
|
Downloading chapters marked as sync offline:
|
||||||
|
|
||||||
## Laravel Sponsors
|
```php
|
||||||
|
php artisan queue:work --queue=downloadVideoFile
|
||||||
|
```
|
||||||
|
Get information about file size of chapters:
|
||||||
|
|
||||||
We would like to extend our thanks to the following sponsors for funding Laravel development. If you are interested in becoming a sponsor, please visit the Laravel [Patreon page](https://patreon.com/taylorotwell).
|
```php
|
||||||
|
php artisan queue:work --queue=getVideoFileSize
|
||||||
### Premium Partners
|
```
|
||||||
|
|
||||||
- **[Vehikl](https://vehikl.com/)**
|
|
||||||
- **[Tighten Co.](https://tighten.co)**
|
|
||||||
- **[Kirschbaum Development Group](https://kirschbaumdevelopment.com)**
|
|
||||||
- **[64 Robots](https://64robots.com)**
|
|
||||||
- **[Cubet Techno Labs](https://cubettech.com)**
|
|
||||||
- **[Cyber-Duck](https://cyber-duck.co.uk)**
|
|
||||||
- **[Many](https://www.many.co.uk)**
|
|
||||||
- **[Webdock, Fast VPS Hosting](https://www.webdock.io/en)**
|
|
||||||
- **[DevSquad](https://devsquad.com)**
|
|
||||||
- **[Curotec](https://www.curotec.com/services/technologies/laravel/)**
|
|
||||||
- **[OP.GG](https://op.gg)**
|
|
||||||
- **[WebReinvent](https://webreinvent.com/?utm_source=laravel&utm_medium=github&utm_campaign=patreon-sponsors)**
|
|
||||||
- **[Lendio](https://lendio.com)**
|
|
||||||
|
|
||||||
## Contributing
|
|
||||||
|
|
||||||
Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions).
|
|
||||||
|
|
||||||
## Code of Conduct
|
|
||||||
|
|
||||||
In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct).
|
|
||||||
|
|
||||||
## Security Vulnerabilities
|
|
||||||
|
|
||||||
If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed.
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).
|
|
||||||
|
|||||||
21
app/Console/Commands/SyncFiles.php
Normal file
21
app/Console/Commands/SyncFiles.php
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Jobs\DownloadVideoFile;
|
||||||
|
use App\Models\Chapter;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
|
class SyncFiles extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'symfonycast:sync';
|
||||||
|
|
||||||
|
protected $description = 'Downloads files locally for chapters marked as sync offline';
|
||||||
|
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
$chapters = Chapter::where('sync_offline', 1)->get();
|
||||||
|
$chapters->each(fn($chapter) => DownloadVideoFile::dispatch($chapter->id));
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
||||||
34
app/Console/Commands/Update.php
Normal file
34
app/Console/Commands/Update.php
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Http\SymfonyCastDl\SymfonyCastDlService;
|
||||||
|
use App\Jobs\DownloadVideoFile;
|
||||||
|
use App\Jobs\GetVideoFileSize;
|
||||||
|
use App\Models\Chapter;
|
||||||
|
use App\Models\Course;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
|
class Update extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'symfonycast:update';
|
||||||
|
|
||||||
|
protected $description = 'Updates information about courses and chapters';
|
||||||
|
|
||||||
|
public function handle(SymfonyCastDlService $symfonyCastDlService)
|
||||||
|
{
|
||||||
|
if (!$symfonyCastDlService->isSubscriptionActive()) {
|
||||||
|
$this->error('To fully update courses and chapters you need active subscription');
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
$this->info('Start updating courses');
|
||||||
|
$courses = $symfonyCastDlService->updateCourses();
|
||||||
|
$this->output->success(count($courses) . ' courses updated');
|
||||||
|
$this->output->info('Start updating each course chapters');
|
||||||
|
$this->withProgressBar($courses, function (Course $course) use ($symfonyCastDlService) {
|
||||||
|
$symfonyCastDlService->updateChapters($course);
|
||||||
|
});
|
||||||
|
$this->output->success('Course chapters updated.');
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
||||||
22
app/Console/Commands/UpdateVideoSize.php
Normal file
22
app/Console/Commands/UpdateVideoSize.php
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Jobs\DownloadVideoFile;
|
||||||
|
use App\Jobs\GetVideoFileSize;
|
||||||
|
use App\Models\Chapter;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
|
class UpdateVideoSize extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'symfonycast:video_size';
|
||||||
|
|
||||||
|
protected $description = 'Updates information about video size';
|
||||||
|
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
$chapters = Chapter::where('video_size', 0)->get();
|
||||||
|
$chapters->each(fn($chapter) => GetVideoFileSize::dispatch($chapter->id));
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
||||||
17
app/Http/Controllers/Chapter/CompleteToggle.php
Normal file
17
app/Http/Controllers/Chapter/CompleteToggle.php
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Chapter;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Chapter;
|
||||||
|
use App\Models\Course;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
|
||||||
|
class CompleteToggle extends Controller
|
||||||
|
{
|
||||||
|
public function __invoke(Course $course, Chapter $chapter): RedirectResponse
|
||||||
|
{
|
||||||
|
$chapter->update(['is_complete' => !$chapter->is_complete]);
|
||||||
|
return redirect()->back();
|
||||||
|
}
|
||||||
|
}
|
||||||
19
app/Http/Controllers/Chapter/Sync.php
Normal file
19
app/Http/Controllers/Chapter/Sync.php
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Chapter;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Jobs\DownloadVideoFile;
|
||||||
|
use App\Models\Chapter;
|
||||||
|
use App\Models\Course;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
|
||||||
|
class Sync extends Controller
|
||||||
|
{
|
||||||
|
public function __invoke(Course $course, Chapter $chapter): RedirectResponse
|
||||||
|
{
|
||||||
|
$chapter->update(['sync_offline' => 1]);
|
||||||
|
DownloadVideoFile::dispatch($chapter->id);
|
||||||
|
return redirect()->back();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,16 +3,24 @@
|
|||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
use App\Http\SymfonyCastDl\SymfonyCastDlService;
|
use App\Http\SymfonyCastDl\SymfonyCastDlService;
|
||||||
use App\Models\Chapter;
|
use App\Models\Course;
|
||||||
|
|
||||||
class ChapterController extends Controller
|
class ChapterController extends Controller
|
||||||
{
|
{
|
||||||
public function index(Chapter $chapter, SymfonyCastDlService $symfonyCastDlService)
|
public function __invoke(Course $course, int $chapter, SymfonyCastDlService $symfonyCastDlService)
|
||||||
{
|
{
|
||||||
|
$chapter = $course->chapters->firstWhere('order', $chapter);
|
||||||
$symfonyCastDlService->videoSize($chapter);
|
$symfonyCastDlService->videoSize($chapter);
|
||||||
|
if ($chapter->sync_offline) {
|
||||||
$symfonyCastDlService->downloadFile($chapter);
|
$symfonyCastDlService->downloadFile($chapter);
|
||||||
// $symfonyCastDlService->videoSize($chapter);
|
$videoUrl = $chapter->video_url;
|
||||||
// file_put_contents('movie.avi', file_get_contents($chapter->video_link));
|
} else {
|
||||||
dd($chapter->toArray());
|
$videoUrl = $symfonyCastDlService->getChapterInfo($chapter);
|
||||||
|
}
|
||||||
|
|
||||||
|
$chapters = $chapter->course->chapters;
|
||||||
|
$next = $chapters->where('order', '>', $chapter->order)->first();
|
||||||
|
$prev = $chapters->where('order', '<', $chapter->order)->sortByDesc('order')->first();
|
||||||
|
return view('chapter.index', compact('chapter', 'chapters', 'prev', 'next', 'videoUrl'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
18
app/Http/Controllers/Course/Sync.php
Normal file
18
app/Http/Controllers/Course/Sync.php
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Course;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Jobs\DownloadVideoFile;
|
||||||
|
use App\Models\Course;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
|
||||||
|
class Sync extends Controller
|
||||||
|
{
|
||||||
|
public function __invoke(Course $course): RedirectResponse
|
||||||
|
{
|
||||||
|
$course->chapters->each->update(['sync_offline' => 1]);
|
||||||
|
$course->chapters->each(fn($chapter) => DownloadVideoFile::dispatch($chapter->id));
|
||||||
|
return redirect()->back();
|
||||||
|
}
|
||||||
|
}
|
||||||
17
app/Http/Controllers/Course/Update.php
Normal file
17
app/Http/Controllers/Course/Update.php
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Course;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\SymfonyCastDl\SymfonyCastDlService;
|
||||||
|
use App\Models\Course;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
|
||||||
|
class Update extends Controller
|
||||||
|
{
|
||||||
|
public function __invoke(SymfonyCastDlService $symfonyCastDlService, Course $course): RedirectResponse
|
||||||
|
{
|
||||||
|
$symfonyCastDlService->updateCourse($course);
|
||||||
|
return redirect()->back();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,16 +11,8 @@ use Illuminate\View\View;
|
|||||||
|
|
||||||
class CourseController extends Controller
|
class CourseController extends Controller
|
||||||
{
|
{
|
||||||
public function index(Course $course): View
|
public function __invoke(Course $course): View
|
||||||
{
|
{
|
||||||
return view('course.index', compact('course'));
|
return view('course.index', compact('course'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function sync(Course $course)
|
|
||||||
{
|
|
||||||
$course->chapters->each->update(['sync_offline' => 1]);
|
|
||||||
$course->chapters->each(fn($chapter) => DownloadVideoFile::dispatch($chapter->id));
|
|
||||||
return redirect(route('course.index', ['course' => $course]));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,20 +5,22 @@ namespace App\Http\Controllers;
|
|||||||
use App\Http\SymfonyCastDl\HtmlParser;
|
use App\Http\SymfonyCastDl\HtmlParser;
|
||||||
use App\Http\SymfonyCastDl\SymfonyCastDlService;
|
use App\Http\SymfonyCastDl\SymfonyCastDlService;
|
||||||
use App\Models\Course;
|
use App\Models\Course;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
class Index extends Controller
|
class Index extends Controller
|
||||||
{
|
{
|
||||||
public function index()
|
public function index(Request $request, string $track = null)
|
||||||
{
|
{
|
||||||
$courses = Course::with('chapters')->withCount('chapters')->get();
|
$courses = Course::where('tracks', 'like', '%' . $track . '%')->with('chapters')->withCount([
|
||||||
|
'chapters',
|
||||||
// dd($courses->toArray());
|
'chapters as chapters_to_sync' => function (Builder $query) {
|
||||||
|
$query->where('sync_offline', 1);
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
$request->whenHas('order', fn($order) => $courses->orderby($order, $request->dir));
|
||||||
|
$request->whenMissing('order', fn() => $courses->orderby('published_at', 'desc'));
|
||||||
|
$courses = $courses->get();
|
||||||
return view('index', compact(['courses']));
|
return view('index', compact(['courses']));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function download(SymfonyCastDlService $symfonyCastDlService)
|
|
||||||
{
|
|
||||||
$symfonyCastDlService->getInfo();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ class HtmlParser
|
|||||||
$course->thumbnail = $courseItem->first('img.course-list-item-img')->attr('src');
|
$course->thumbnail = $courseItem->first('img.course-list-item-img')->attr('src');
|
||||||
$course->link = last(explode('/', $courseItem->first('a')->attr('href')));
|
$course->link = last(explode('/', $courseItem->first('a')->attr('href')));
|
||||||
$course->status = $courseItem->attr('data-status');
|
$course->status = $courseItem->attr('data-status');
|
||||||
|
$course->tracks = $courseItem->attr('data-tracks');
|
||||||
$course->course_id = $courseItem->attr('data-id');
|
$course->course_id = $courseItem->attr('data-id');
|
||||||
$course->numberofchapters = $courseItem->attr('data-chapter-count');
|
$course->numberofchapters = $courseItem->attr('data-chapter-count');
|
||||||
$course->timeswatched = $courseItem->attr('data-times-watched');
|
$course->timeswatched = $courseItem->attr('data-times-watched');
|
||||||
@@ -48,7 +49,6 @@ class HtmlParser
|
|||||||
foreach ($document->find('ul.chapter-list li') as $chapterItem) {
|
foreach ($document->find('ul.chapter-list li') as $chapterItem) {
|
||||||
if ($chapterItem->first('.col')) {
|
if ($chapterItem->first('.col')) {
|
||||||
$chapterId++;
|
$chapterId++;
|
||||||
|
|
||||||
$chapter = Chapter::firstOrNew(['course_id' => $courseId, 'order' => $chapterId]);
|
$chapter = Chapter::firstOrNew(['course_id' => $courseId, 'order' => $chapterId]);
|
||||||
$chapter->duration = $chapterItem->first('.length-styling')?->text();
|
$chapter->duration = $chapterItem->first('.length-styling')?->text();
|
||||||
$chapter->order = $chapterId;
|
$chapter->order = $chapterId;
|
||||||
@@ -62,8 +62,17 @@ class HtmlParser
|
|||||||
$chapters->add($chapter);
|
$chapters->add($chapter);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $chapters;
|
return $chapters;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getVideoSource(Response $respose): ?string
|
||||||
|
{
|
||||||
|
return (new Document($respose->getBody()->getContents()))->first('video source')?->getAttribute('src');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isSubscriptionActive(Response $respose): bool
|
||||||
|
{
|
||||||
|
return (new Document($respose->getBody()->getContents()))->has('.subscription-status .i-active');
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,11 @@ namespace App\Http\SymfonyCastDl;
|
|||||||
use App\Jobs\GetVideoFileSize;
|
use App\Jobs\GetVideoFileSize;
|
||||||
use App\Models\Chapter;
|
use App\Models\Chapter;
|
||||||
use App\Models\Course;
|
use App\Models\Course;
|
||||||
|
use GuzzleHttp\Exception\GuzzleException;
|
||||||
use GuzzleHttp\TransferStats;
|
use GuzzleHttp\TransferStats;
|
||||||
use GuzzleHttp\Client;
|
use GuzzleHttp\Client;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
use JetBrains\PhpStorm\NoReturn;
|
||||||
|
|
||||||
class SymfonyCastDlService
|
class SymfonyCastDlService
|
||||||
{
|
{
|
||||||
@@ -18,11 +21,8 @@ class SymfonyCastDlService
|
|||||||
'base_uri' => config('symfonycast.base_url'),
|
'base_uri' => config('symfonycast.base_url'),
|
||||||
'cookies' => true
|
'cookies' => true
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$response = $this->client->get('login');
|
$response = $this->client->get('login');
|
||||||
|
|
||||||
$token = $htmlParser->getCsrfToken($response);
|
$token = $htmlParser->getCsrfToken($response);
|
||||||
|
|
||||||
$this->client->post('login', [
|
$this->client->post('login', [
|
||||||
'form_params' => [
|
'form_params' => [
|
||||||
'email' => config('symfonycast.login'),
|
'email' => config('symfonycast.login'),
|
||||||
@@ -35,47 +35,71 @@ class SymfonyCastDlService
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getInfo()
|
/**
|
||||||
|
* @return Collection
|
||||||
|
* @throws GuzzleException
|
||||||
|
*/
|
||||||
|
public function updateCourses(): Collection
|
||||||
{
|
{
|
||||||
$coursePage = $this->client->get('courses/filtering');
|
$coursePage = $this->client->get('courses/filtering');
|
||||||
|
|
||||||
$courses = $this->htmlParser->getCourses($coursePage);
|
$courses = $this->htmlParser->getCourses($coursePage);
|
||||||
$courses->each->save();
|
return $courses->each->save();
|
||||||
/** @var Course $course */
|
}
|
||||||
foreach ($courses as $course) {
|
|
||||||
|
/**
|
||||||
|
* @param Course $course
|
||||||
|
* @return Collection
|
||||||
|
* @throws GuzzleException
|
||||||
|
*/
|
||||||
|
public function updateChapters(Course $course): Collection
|
||||||
|
{
|
||||||
$singleCoursePage = $this->client->get('/screencast/' . $course->link);
|
$singleCoursePage = $this->client->get('/screencast/' . $course->link);
|
||||||
$chapters = $this->htmlParser->getCourseDetails($singleCoursePage, $course->id);
|
$chapters = $this->htmlParser->getCourseDetails($singleCoursePage, $course->id);
|
||||||
$chapters->each->save();
|
$chapters->each->save();
|
||||||
$chapters->each(fn($chapter) => GetVideoFileSize::dispatch($chapter->id));
|
$chapters->each(fn($chapter) => GetVideoFileSize::dispatch($chapter->id));
|
||||||
|
return $chapters;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function isSubscriptionActive(): bool
|
||||||
|
{
|
||||||
|
return $this->htmlParser->isSubscriptionActive($this->client->get('/profile/show'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getChapterInfo(Chapter $chapter): ?string
|
||||||
|
{
|
||||||
|
$course = $chapter->course;
|
||||||
|
$chapterPage = $this->client->get('/screencast/' . $course->link . DIRECTORY_SEPARATOR . $chapter->link);
|
||||||
|
return $this->htmlParser->getVideoSource($chapterPage);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function videoSize(Chapter $chapter): Chapter
|
public function videoSize(Chapter $chapter): Chapter
|
||||||
{
|
{
|
||||||
try {
|
if (!$chapter->video_size && $chapter->video_link) {
|
||||||
if (!$chapter->video_size) {
|
|
||||||
echo 1;
|
|
||||||
$response = $this->client->head($chapter->video_link);
|
$response = $this->client->head($chapter->video_link);
|
||||||
if ($response->hasHeader('Content-Length')) {
|
if ($response->hasHeader('Content-Length')) {
|
||||||
$chapter->video_size = $response->getHeader('Content-Length')[0];
|
$chapter->video_size = (int)$response->getHeader('Content-Length')[0];
|
||||||
}
|
|
||||||
$chapter->save();
|
$chapter->save();
|
||||||
}
|
}
|
||||||
} catch (\Exception $exception) {
|
|
||||||
}
|
}
|
||||||
return $chapter;
|
return $chapter;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function downloadFile(Chapter $chapter): bool
|
public function downloadFile(Chapter $chapter): void
|
||||||
{
|
{
|
||||||
if (!is_dir($chapter->course_id . '/')) {
|
if (is_null($chapter->video_link)) {
|
||||||
mkdir($chapter->course_id);
|
return;
|
||||||
}
|
}
|
||||||
|
if (!is_dir($chapter->directory_path)) {
|
||||||
|
mkdir(directory: $chapter->directory_path, recursive: true);
|
||||||
|
}
|
||||||
|
if (!$chapter->is_video_file) {
|
||||||
$this->client->request(
|
$this->client->request(
|
||||||
'GET',
|
'GET',
|
||||||
$chapter->video_link,
|
$chapter->video_link,
|
||||||
['sink' => $chapter->course_id . '/' . $chapter->id . '.mp4']
|
['sink' => $chapter->video_path],
|
||||||
);
|
);
|
||||||
return true;
|
}
|
||||||
|
$chapter->video_size = filesize($chapter->video_path);
|
||||||
|
$chapter->save();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,17 +11,24 @@ use Illuminate\Foundation\Bus\Dispatchable;
|
|||||||
use Illuminate\Queue\InteractsWithQueue;
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
use Illuminate\Queue\SerializesModels;
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
|
||||||
class DownloadVideoFile implements ShouldQueue
|
class DownloadVideoFile implements ShouldQueue, ShouldBeUnique
|
||||||
{
|
{
|
||||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||||
|
|
||||||
|
public const NAME = 'downloadVideoFile';
|
||||||
|
|
||||||
public function __construct(private int $chapterId)
|
public function __construct(private int $chapterId)
|
||||||
{
|
{
|
||||||
//
|
$this->onQueue(self::NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function uniqueId(): string
|
||||||
|
{
|
||||||
|
return $this->chapterId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function handle(SymfonyCastDlService $symfonyCastDlService)
|
public function handle(SymfonyCastDlService $symfonyCastDlService)
|
||||||
{
|
{
|
||||||
$symfonyCastDlService->videoSize(Chapter::find($this->chapterId));
|
$symfonyCastDlService->downloadFile(Chapter::find($this->chapterId));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
namespace App\Jobs;
|
namespace App\Jobs;
|
||||||
|
|
||||||
use App\Http\SymfonyCastDl\HtmlParser;
|
|
||||||
use App\Http\SymfonyCastDl\SymfonyCastDlService;
|
use App\Http\SymfonyCastDl\SymfonyCastDlService;
|
||||||
use App\Models\Chapter;
|
use App\Models\Chapter;
|
||||||
use Illuminate\Bus\Queueable;
|
use Illuminate\Bus\Queueable;
|
||||||
@@ -12,12 +11,20 @@ use Illuminate\Foundation\Bus\Dispatchable;
|
|||||||
use Illuminate\Queue\InteractsWithQueue;
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
use Illuminate\Queue\SerializesModels;
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
|
||||||
class GetVideoFileSize implements ShouldQueue
|
class GetVideoFileSize implements ShouldQueue, ShouldBeUnique
|
||||||
{
|
{
|
||||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||||
|
|
||||||
|
public const NAME = 'getVideoFileSize';
|
||||||
|
|
||||||
public function __construct(private int $chapterId)
|
public function __construct(private int $chapterId)
|
||||||
{
|
{
|
||||||
|
$this->onQueue(self::NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function uniqueId(): string
|
||||||
|
{
|
||||||
|
return $this->chapterId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function handle(SymfonyCastDlService $symfonyCastDlService)
|
public function handle(SymfonyCastDlService $symfonyCastDlService)
|
||||||
|
|||||||
@@ -2,11 +2,13 @@
|
|||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @property int $id
|
||||||
* @property int $order
|
* @property int $order
|
||||||
* @property string $link
|
* @property string $link
|
||||||
* @property string $video_link
|
* @property string $video_link
|
||||||
@@ -15,6 +17,10 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|||||||
* @property string $duration
|
* @property string $duration
|
||||||
* @property integer $course_id
|
* @property integer $course_id
|
||||||
* @property bool $sync_offline
|
* @property bool $sync_offline
|
||||||
|
* @property bool $is_complete
|
||||||
|
* @property string $video_path
|
||||||
|
* @property string $directory_path
|
||||||
|
* @property Course $course
|
||||||
*/
|
*/
|
||||||
class Chapter extends Model
|
class Chapter extends Model
|
||||||
{
|
{
|
||||||
@@ -29,10 +35,53 @@ class Chapter extends Model
|
|||||||
'duration',
|
'duration',
|
||||||
'course_id',
|
'course_id',
|
||||||
'sync_offline',
|
'sync_offline',
|
||||||
|
'is_complete',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
protected $appends = ['video_path', 'directory_path', 'video_url'];
|
||||||
|
|
||||||
public function course(): BelongsTo
|
public function course(): BelongsTo
|
||||||
{
|
{
|
||||||
return $this->belongsTo(Course::class);
|
return $this->belongsTo(Course::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function videoUrl(): Attribute
|
||||||
|
{
|
||||||
|
return Attribute::make(
|
||||||
|
get: fn() => url(
|
||||||
|
'videos' . DIRECTORY_SEPARATOR . $this->course_id . DIRECTORY_SEPARATOR . $this->order . '.mp4'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function videoPath(): Attribute
|
||||||
|
{
|
||||||
|
return Attribute::make(
|
||||||
|
get: fn() => $this->directory_path . '/' . $this->order . '.mp4'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function directoryPath(): Attribute
|
||||||
|
{
|
||||||
|
return Attribute::make(
|
||||||
|
get: fn() => public_path('videos' . DIRECTORY_SEPARATOR . $this->course_id)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function isVideoFile(): Attribute
|
||||||
|
{
|
||||||
|
return Attribute::make(
|
||||||
|
get: fn() => is_file($this->video_path)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function videoSizeHuman(): Attribute
|
||||||
|
{
|
||||||
|
return Attribute::make(
|
||||||
|
get: function () {
|
||||||
|
return formatFileSize($this->video_size);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||||
use Carbon\Traits\Date;
|
use Carbon\Traits\Date;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
@@ -13,9 +14,11 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
|
|||||||
* @property string $thumbnail
|
* @property string $thumbnail
|
||||||
* @property string $link
|
* @property string $link
|
||||||
* @property string $status
|
* @property string $status
|
||||||
|
* @property string $tracks
|
||||||
* @property integer $numberofchapters
|
* @property integer $numberofchapters
|
||||||
* @property integer $timeswatched
|
* @property integer $timeswatched
|
||||||
* @property Date $published_at
|
* @property Date $published_at
|
||||||
|
* @property Chapter[] $chapters
|
||||||
*/
|
*/
|
||||||
class Course extends Model
|
class Course extends Model
|
||||||
{
|
{
|
||||||
@@ -26,11 +29,13 @@ class Course extends Model
|
|||||||
'thumbnail',
|
'thumbnail',
|
||||||
'link',
|
'link',
|
||||||
'status',
|
'status',
|
||||||
|
'tracks',
|
||||||
'numberofchapters',
|
'numberofchapters',
|
||||||
'timeswatched',
|
'timeswatched',
|
||||||
'published_at',
|
'published_at',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
protected $appends = ['total_size', 'total_size_human'];
|
||||||
protected $dates = [
|
protected $dates = [
|
||||||
'published_at'
|
'published_at'
|
||||||
];
|
];
|
||||||
@@ -39,4 +44,25 @@ class Course extends Model
|
|||||||
{
|
{
|
||||||
return $this->hasMany(Chapter::class);
|
return $this->hasMany(Chapter::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function totalSize(): Attribute
|
||||||
|
{
|
||||||
|
return Attribute::make(
|
||||||
|
get: fn() => $this->chapters->sum('video_size'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function tracks(): Attribute
|
||||||
|
{
|
||||||
|
return Attribute::make(
|
||||||
|
get: fn(string $tracks) => empty($tracks) ? [] : explode(',', $tracks),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function totalSizeHuman(): Attribute
|
||||||
|
{
|
||||||
|
return Attribute::make(
|
||||||
|
get: fn() => formatFileSize($this->total_size),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
22
app/helpers.php
Normal file
22
app/helpers.php
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
if (!function_exists('formatFileSize')) {
|
||||||
|
function formatFileSize($fileSizeInBytes): string
|
||||||
|
{
|
||||||
|
$base = log($fileSizeInBytes) / log(1024);
|
||||||
|
$suffix = ["", "k", "M", "G", "T"][floor($base)];
|
||||||
|
$size = round(pow(1024, $base - floor($base)), 2);
|
||||||
|
return !is_nan($size) ? $size . $suffix : '0B';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!function_exists('folderSize')) {
|
||||||
|
function folderSize($dir): int
|
||||||
|
{
|
||||||
|
$size = 0;
|
||||||
|
foreach (glob(rtrim($dir, '/') . '/*', GLOB_NOSORT) as $each) {
|
||||||
|
$size += is_file($each) ? filesize($each) : folderSize($each);
|
||||||
|
}
|
||||||
|
return $size;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,10 +2,14 @@
|
|||||||
"name": "laravel/laravel",
|
"name": "laravel/laravel",
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"description": "The Laravel Framework.",
|
"description": "The Laravel Framework.",
|
||||||
"keywords": ["framework", "laravel"],
|
"keywords": [
|
||||||
|
"framework",
|
||||||
|
"laravel"
|
||||||
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"require": {
|
"require": {
|
||||||
"php": "^8.0.2",
|
"php": "^8.0.2",
|
||||||
|
"doctrine/dbal": "^3.5",
|
||||||
"guzzlehttp/guzzle": "^7.2",
|
"guzzlehttp/guzzle": "^7.2",
|
||||||
"imangazaliev/didom": "^2.0",
|
"imangazaliev/didom": "^2.0",
|
||||||
"laravel/framework": "^9.19",
|
"laravel/framework": "^9.19",
|
||||||
@@ -19,6 +23,7 @@
|
|||||||
"laravel/sail": "^1.0.1",
|
"laravel/sail": "^1.0.1",
|
||||||
"mockery/mockery": "^1.4.4",
|
"mockery/mockery": "^1.4.4",
|
||||||
"nunomaduro/collision": "^6.1",
|
"nunomaduro/collision": "^6.1",
|
||||||
|
"phpspec/phpspec": "^7.4",
|
||||||
"phpunit/phpunit": "^9.5.10",
|
"phpunit/phpunit": "^9.5.10",
|
||||||
"spatie/laravel-ignition": "^1.0"
|
"spatie/laravel-ignition": "^1.0"
|
||||||
},
|
},
|
||||||
@@ -27,7 +32,10 @@
|
|||||||
"App\\": "app/",
|
"App\\": "app/",
|
||||||
"Database\\Factories\\": "database/factories/",
|
"Database\\Factories\\": "database/factories/",
|
||||||
"Database\\Seeders\\": "database/seeders/"
|
"Database\\Seeders\\": "database/seeders/"
|
||||||
}
|
},
|
||||||
|
"files": [
|
||||||
|
"app/helpers.php"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"autoload-dev": {
|
"autoload-dev": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
|
|||||||
2295
composer.lock
generated
2295
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('courses', function (Blueprint $table) {
|
||||||
|
$table->text('tracks')->after('status')->nullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('courses', function (Blueprint $table) {
|
||||||
|
$table->removeColumn('tracks');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('chapters', function (Blueprint $table) {
|
||||||
|
$table->boolean('is_complete')->default(0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('chapters', function (Blueprint $table) {
|
||||||
|
$table->removeColumn('is_complete');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
84
resources/views/chapter/index.blade.php
Normal file
84
resources/views/chapter/index.blade.php
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
<x-layout>
|
||||||
|
|
||||||
|
<h1>
|
||||||
|
<a href="/" class="text-decoration-none">List</a> » <a class="text-decoration-none"
|
||||||
|
href="{{ route('course.index', ['course' => $chapter->course_id]) }}">{{ $chapter->course->name }}</a>
|
||||||
|
<br>
|
||||||
|
<small class="text-muted">{{ $chapter->title }}</small>
|
||||||
|
</h1>
|
||||||
|
<div>
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
@if($prev)
|
||||||
|
<div class="float-start m-2"><a
|
||||||
|
href="{{ route('course.chapter', ['course' => $chapter->course_id, 'chapter' => $prev->order]) }}"
|
||||||
|
class="btn btn-primary">Prev</a>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
@if($videoUrl)
|
||||||
|
<div class="d-flex justify-content-center flex-column">
|
||||||
|
<div class="p-2">
|
||||||
|
|
||||||
|
<video class="mx-auto" width="500" controls>
|
||||||
|
<source src="{{ $videoUrl }}" type="video/mp4"/>
|
||||||
|
</video>
|
||||||
|
</div>
|
||||||
|
<div class="p-2">
|
||||||
|
<input type="range" id="playback_range" min="0.5" max="4" value="1" step="0.25">
|
||||||
|
<label for="cowbell">playback speed</label>
|
||||||
|
<span id="playback"> </span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
no video
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<table class="table table-sm">
|
||||||
|
@foreach($chapters as $courseChapter)
|
||||||
|
<tr class="@if($courseChapter->id === $chapter->id)table-dark @endif">
|
||||||
|
<td><a href="{{ route('course.chapter.complete_toggle', ['course' => $courseChapter->course->id, 'chapter' => $courseChapter->id]) }}" class="text-decoration-none"><span class="icon-check {{ $courseChapter->is_complete?'text-info':'text-dark' }}"></span></a></td>
|
||||||
|
<td>
|
||||||
|
<a href="{{ route('course.chapter', ['course' => $courseChapter->course_id, 'chapter' => $courseChapter->order ]) }}"
|
||||||
|
class="text-decoration-none">{{ $courseChapter->title }}</a>
|
||||||
|
</td>
|
||||||
|
<td class="text-end">{{ $courseChapter->duration }}</td>
|
||||||
|
</tr>
|
||||||
|
@endforeach
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
@if($next)
|
||||||
|
<div class="float-end m-2"><a
|
||||||
|
href="{{ route('course.chapter', ['course' => $chapter->course_id, 'chapter' => $next->order]) }}"
|
||||||
|
class="btn btn-primary">Next</a>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
const value = document.querySelector("#playback")
|
||||||
|
const input = document.querySelector("#playback_range")
|
||||||
|
document.querySelector('body').onload = function () {
|
||||||
|
const playbackRate = localStorage['playback'] || 1;
|
||||||
|
document.querySelector('video').playbackRate = playbackRate;
|
||||||
|
document.querySelector('#playback_range').value = playbackRate;
|
||||||
|
value.textContent = 'x' + playbackRate;
|
||||||
|
};
|
||||||
|
|
||||||
|
value.textContent = 'x' + input.value
|
||||||
|
input.addEventListener("input", (event) => {
|
||||||
|
localStorage['playback'] = event.target.value;
|
||||||
|
value.textContent = 'x' + event.target.value;
|
||||||
|
document.querySelector('video').playbackRate = event.target.value;
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
</x-layout>
|
||||||
@@ -2,9 +2,11 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<title>{{ $title ?? 'Symfonycast.local' }}</title>
|
<title>{{ $title ?? 'Symfonycast.local' }}</title>
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet"
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet"
|
||||||
integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
|
crossorigin="anonymous">
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap-dark-5@1.1.3/dist/css/bootstrap-nightfall.min.css"
|
||||||
|
rel="stylesheet" media="(prefers-color-scheme: dark)">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/simple-line-icons/2.5.5/css/simple-line-icons.min.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
{{ $slot }}
|
{{ $slot }}
|
||||||
|
|||||||
@@ -1,27 +1,45 @@
|
|||||||
<x-layout>
|
<x-layout>
|
||||||
<h1>
|
<h1>
|
||||||
{{ $course->name }}
|
<a href="/" class="text-decoration-none">List</a> » {{ $course->name }}
|
||||||
</h1>
|
</h1>
|
||||||
|
<a href="{{ 'https://symfonycasts.com/screencast/'.$course->link }}">Visit course page</a>
|
||||||
|
|
||||||
<a href="{{ route('course.sync', ['course' => $course->id]) }}">Sync all chapters offline</a>
|
<a href="{{ route('course.sync', ['course' => $course->id]) }}">Sync all chapters offline</a>
|
||||||
|
<a href="{{ route('course.update', ['course' => $course->id]) }}">Update course</a>
|
||||||
<table class="table">
|
<table class="table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Title</th>
|
<th>Title</th>
|
||||||
<th>Duration</th>
|
<th>Duration</th>
|
||||||
<th>Sync offline</th>
|
<th>Sync offline</th>
|
||||||
|
<th>Synced offline</th>
|
||||||
|
<th>File size</th>
|
||||||
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@foreach($course->chapters()->get() as $chapter)
|
@foreach($course->chapters()->get() as $chapter)
|
||||||
<tr>
|
<tr>
|
||||||
<td><a href="{{ route('course.chapter', ['chapter' => $chapter->id]) }}">{{ $chapter->title }}</a></td>
|
<td><a href="{{ route('course.chapter', ['course' => $course->id, 'chapter' => $chapter->order]) }}"
|
||||||
<td>{{ $chapter->sync_offline?'Yes':'No' }}</td>
|
class="text-decoration-none">{{ $chapter->title }}</a></td>
|
||||||
|
<td><a href="{{ route('course.chapter.complete_toggle', ['course' => $course->id, 'chapter' => $chapter->id]) }}" class="text-decoration-none"><span class="icon-check {{ $chapter->is_complete?'text-info':'text-dark' }}"></span></a></td>
|
||||||
<td>{{ $chapter->duration }}</td>
|
<td>{{ $chapter->duration }}</td>
|
||||||
|
<td>{{ $chapter->sync_offline?'Yes':'no' }}</td>
|
||||||
|
<td>{{ $chapter->is_video_file?'Yes':'-' }}</td>
|
||||||
|
<td>{{ $chapter->video_size_human }}</td>
|
||||||
|
<td class="text-end">
|
||||||
|
@if(!$chapter->sync_offline)
|
||||||
|
<a
|
||||||
|
href="{{ route('course.chapter.sync', ['course' => $chapter->course->id,'chapter' => $chapter->id]) }}">sync</a>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@endforeach
|
@endforeach
|
||||||
<tr>
|
<tr>
|
||||||
<td></td>
|
<td></td>
|
||||||
<td>Total: {{ $course->course_duration }}</td>
|
<td>Total: {{ $course->course_duration }}</td>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
<td>Size: {{ $course->total_size_human }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</x-layout>
|
</x-layout>
|
||||||
|
|||||||
@@ -1,25 +1,54 @@
|
|||||||
<x-layout>
|
<x-layout>
|
||||||
<h1>List of Courses</h1>
|
<h1><a class="text-decoration-none" href="{{route('index')}}">List of Courses</a> <small
|
||||||
|
class="text-muted">{{ request()->has('track')?:request()->track }}</small></h1>
|
||||||
<table class="table table-sm">
|
<table class="table table-sm">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Id</th>
|
|
||||||
<th></th>
|
<th></th>
|
||||||
<th>Name</th>
|
<th>
|
||||||
<th>status</th>
|
|
||||||
<th>Chapters</th>
|
<a href="{{ route('index', ['order' => 'name', 'dir' => request()->whenHas('dir', fn($dir) => ['asc'=>'desc', 'desc'=>'asc'][$dir], fn() => 'desc')]) }}">Name</a>
|
||||||
<th>Published at</th>
|
</th>
|
||||||
|
<th>Tracks</th>
|
||||||
|
<th>Sync</th>
|
||||||
|
<th>Estimate file size</th>
|
||||||
|
<th>Actual size</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@foreach($courses as $course)
|
@foreach($courses as $course)
|
||||||
<tr>
|
<tr>
|
||||||
<td class="align-middle">{{ $course->id }}</td>
|
<td><img src="{{ $course->thumbnail }}" alt="{{ $course->name }}" class="img-thumbnail"
|
||||||
<td><img src="{{ $course->thumbnail }}" alt="{{ $course->name }}" class="img-thumbnail" style="width: 70px;"></td>
|
style="width: 70px;"></td>
|
||||||
<td class="align-middle"><a href="{{ route('course.index', ['course' => $course->id]) }}">{{ $course->name }}</a></td>
|
<td class="align-middle">
|
||||||
<td class="align-middle">{{ $course->status }}</td>
|
<a class="text-decoration-none"
|
||||||
<td class="align-middle">{{ $course->chapters_count }} / {{ $course->numberofchapters }}</td>
|
href="{{ route('course.index', ['course' => $course->id]) }}">{{ $course->name }}</a>
|
||||||
<td class="align-middle"><abbr title="{{ $course->published_at?->format('Y-m-d') }}">{{ $course->published_at?->diffForHumans() }}</abbr></td>
|
<p class="">
|
||||||
|
course status: <span class="fw-lighter">{{ $course->status }}</span>
|
||||||
|
published: <span class="fw-lighter"><a
|
||||||
|
href="?order=published_at&dir={{ request()->has('dir')? ['asc'=>'desc', 'desc'=>'asc'][request()->dir]: 'desc'}}"><abbr
|
||||||
|
title="{{ $course->published_at?->format('Y-m-d') }}">{{ $course->published_at?->diffForHumans() }}</abbr></a></span>
|
||||||
|
synced chapters: <span
|
||||||
|
class="fw-lighter">{{ $course->chapters_to_sync }} of {{ $course->numberofchapters }}</span>
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
<td class="align-middle">
|
||||||
|
@foreach($course->tracks as $track)
|
||||||
|
|
||||||
|
<a href="{{ route('index.track', ['track' => $track]) }}"
|
||||||
|
class="badge rounded-pill bg-secondary text-decoration-none">{{ $track }}</a>
|
||||||
|
@endforeach
|
||||||
|
</td>
|
||||||
|
<td class="align-middle"><a class="btn btn-outline-primary"
|
||||||
|
href="{{ route('course.sync', ['course' => $course->id]) }}">Sync all
|
||||||
|
chapters offline</a></td>
|
||||||
|
<td class="align-middle text-end pe-4">{{ $course->total_size_human }}</td>
|
||||||
|
<td class="align-middle text-end pe-4">{{formatFileSize(folderSize(public_path('videos'.DIRECTORY_SEPARATOR.$course->id)))}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
@endforeach
|
@endforeach
|
||||||
|
<tr>
|
||||||
|
<td colspan="4"></td>
|
||||||
|
<td class="align-middle text-end pe-4">{{ formatFileSize($courses->sum('total_size')) }}</td>
|
||||||
|
<td class="align-middle text-end pe-4">{{formatFileSize(folderSize(public_path()))}}</td>
|
||||||
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</x-layout>
|
</x-layout>
|
||||||
|
|||||||
@@ -1,27 +1,22 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use App\Http\Controllers\ChapterController;
|
use App\Http\Controllers\ChapterController;
|
||||||
|
use App\Http\Controllers\Course;
|
||||||
|
use App\Http\Controllers\Chapter;
|
||||||
use App\Http\Controllers\CourseController;
|
use App\Http\Controllers\CourseController;
|
||||||
use App\Http\Controllers\Index;
|
use App\Http\Controllers\Index;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Web Routes
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| Here is where you can register web routes for your application. These
|
|
||||||
| routes are loaded by the RouteServiceProvider within a group which
|
|
||||||
| contains the "web" middleware group. Now create something great!
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
Route::get('/download', [Index::class, 'download']);
|
Route::get('/download', [Index::class, 'download']);
|
||||||
Route::get('/', [Index::class, 'index']);
|
Route::get('/', [Index::class, 'index'])->name('index');
|
||||||
Route::prefix('course')->name('course.')->group(function () {
|
Route::get('/track/{track}', [Index::class, 'index'])->name('index.track');
|
||||||
Route::get('/{course}', [CourseController::class, 'index'])->name('index');
|
Route::prefix('course')->group(function () {
|
||||||
Route::get('/{course}/sync', [CourseController::class, 'sync'])->name('sync');
|
Route::get('/{course}', CourseController::class)->name('course.index');
|
||||||
|
Route::get('/{course}/sync', Course\Sync::class)->name('course.sync');
|
||||||
|
Route::get('/{course}/update', Course\Update::class)->name('course.update');
|
||||||
|
Route::get('/{course}/chapter/{chapter}', ChapterController::class)->name('course.chapter');
|
||||||
|
Route::get('/{course}/chapter/{chapter}/sync', Chapter\Sync::class)->name('course.chapter.sync');
|
||||||
|
Route::get('/{course}/chapter/{chapter}/completetoggle', Chapter\CompleteToggle::class)->name('course.chapter.complete_toggle');
|
||||||
});
|
});
|
||||||
Route::get('/chapter/{chapter}', [ChapterController::class, 'index'])->name('course.chapter');
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user