├── .github
└── FUNDING.yml
├── .gitignore
├── LICENSE.md
├── README.md
├── composer.json
├── config
└── youtube-api.php
├── database
└── migrations
│ ├── 2021_10_06_094035_create_logs_table.php
│ └── 2021_10_31_125901_add_user_to_log.php
├── resources
└── views
│ ├── home
│ └── index.blade.php
│ ├── logs
│ └── index.blade.php
│ └── template.blade.php
├── routes
├── api.php
└── web.php
├── src
├── Controllers
│ ├── ApiController.php
│ └── HomeController.php
├── Drivers
│ ├── Cobalt.php
│ ├── IDriver.php
│ └── Local.php
├── Models
│ ├── Log.php
│ └── Video.php
└── YoutubeAPIServiceProvider.php
└── tests
└── ApiTest.php
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: MichaelBelgium
4 | custom: https://www.paypal.com/donate/?hosted_button_id=ZJSKX2ASR3ARL
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | build
2 | composer.lock
3 | docs
4 | vendor
5 | coverage
6 | .vscode
7 | .idea
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) Michael V.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Laravel Youtube API
2 |
3 | [](https://packagist.org/packages/michaelbelgium/laravel-youtube-api)
4 | [](https://packagist.org/packages/michaelbelgium/laravel-youtube-api)
5 |
6 | This package provides a simple youtube api for your Laravel application. It is based on my non-laravel package [Youtube API](https://github.com/MichaelBelgium/Youtube-to-mp3-API).
7 |
8 | ## Installation
9 |
10 | * Install the package via composer:
11 |
12 | ```bash
13 | composer require michaelbelgium/laravel-youtube-api
14 | ```
15 |
16 | * Optional: publish the config file and edit if u like:
17 | ```bash
18 | php artisan vendor:publish --tag=youtube-api-config
19 | ```
20 |
21 | * This package uses [the public disk](https://laravel.com/docs/8.x/filesystem#the-public-disk) of Laravel. Run this command to create a symbolic link to the public folder so that converted Youtube downloads are accessible:
22 | ```bash
23 | php artisan storage:link
24 | ```
25 |
26 | * Execute package migrations
27 | ```
28 | php artisan migrate
29 | ```
30 |
31 | # Software
32 |
33 | Depending on what driver you use - on the server where your laravel app is located, you'll need to install some packages.
34 |
35 | * Install ffmpeg (+ libmp3lame - see [this wiki](https://github.com/MichaelBelgium/Youtube-to-mp3-API/wiki/Installing-"ffmpeg"-and-"libmp3lame"-manually) for tutorial)
36 | * [install youtube-dl](http://ytdl-org.github.io/youtube-dl/download.html)
37 |
38 | # API Usage
39 |
40 | This package adds 3 api routes. The route prefix, `/ytconverter/` in this case, is configurable.
41 |
42 | * POST|GET /ytconverter/convert
43 | * DELETE /ytconverter/{id}
44 | * GET /ytconverter/search
45 | * GET /ytconverter/info
46 |
47 | Check the wiki page of this repository for more information about the routes.
48 |
49 | To enable the search endpoint you need to acquire a Google API key on the [Google Developer Console](https://console.developers.google.com) for the API "Youtube Data API v3". Use this key in the environment variable `GOOGLE_API_KEY`
50 |
51 | # Configuration
52 |
53 | ## Driver
54 |
55 | Downloading Youtube video's is not simple these days. To cover this, you can choose how you want to download the video's by setting the `driver` in the configuration.
56 |
57 | Available drivers:
58 | * local
59 |
60 | The default driver. Requires ffmpeg and yt-dlp or youtube-dl to be installed on the server and it'll download files to the server. Metadata comes from yt-dlp.
61 |
62 | * cobalt
63 |
64 | Requires a self hosted [Cobalt](https://github.com/imputnet/cobalt) (API) instance. It doesn't require any software to be installed on the server and it doesn't download files to the server.
65 | If `GOOGLE_API_KEY` is set, it'll use the Youtube Data API to get metadata, otherwise it'll download the video to get the metadata and thus use storage space instead.
66 |
67 | ## API authorization
68 |
69 | If needed, you can protect the API routes with an authentication guard by setting `auth` in the configuration.
70 |
71 | Example:
72 | ```PHP
73 | 'auth' => 'sanctum',
74 | ```
75 |
76 | ## API rate limiting
77 |
78 | If needed, you can limit API calls by editing the config setting `ratelimiter`. See [Laravel docs](https://laravel.com/docs/8.x/routing#rate-limiting) for more information or examples.
79 |
80 | Example:
81 |
82 | ```PHP
83 | 'ratelimiter' => function (Request $request) {
84 | return Limit::perMinute(5);
85 | },
86 | ```
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "michaelbelgium/laravel-youtube-api",
3 | "description": "Add a youtube api to your Laravel instance",
4 | "keywords": [
5 | "michaelbelgium",
6 | "laravel-youtube-api",
7 | "youtube-api",
8 | "laravel",
9 | "youtube"
10 | ],
11 | "homepage": "https://github.com/michaelbelgium/laravel-youtube-api",
12 | "license": "MIT",
13 | "type": "library",
14 | "authors": [
15 | {
16 | "name": "Michael V.",
17 | "email": "michael@michaelbelgium.me",
18 | "role": "Developer"
19 | }
20 | ],
21 | "require": {
22 | "google/apiclient": "^2.4",
23 | "illuminate/support": "^8.0|^9.0|^10.0",
24 | "james-heinrich/getid3": "^1.9",
25 | "norkunas/youtube-dl-php": "^2.0"
26 | },
27 | "require-dev": {
28 | "orchestra/testbench": "^6.0",
29 | "phpunit/phpunit": "^9.0|^10.0"
30 | },
31 | "autoload": {
32 | "psr-4": {
33 | "MichaelBelgium\\YoutubeAPI\\": "src"
34 | }
35 | },
36 | "autoload-dev": {
37 | "psr-4": {
38 | "MichaelBelgium\\YoutubeAPI\\Tests\\": "tests"
39 | }
40 | },
41 | "config": {
42 | "sort-packages": true
43 | },
44 | "extra": {
45 | "laravel": {
46 | "providers": [
47 | "MichaelBelgium\\YoutubeAPI\\YoutubeAPIServiceProvider"
48 | ]
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/config/youtube-api.php:
--------------------------------------------------------------------------------
1 | env('YOUTUBE_API_DRIVER', 'local'),
15 |
16 | /*
17 | |--------------------------------------------------------------------------
18 | | Cobalt server configuration
19 | |--------------------------------------------------------------------------
20 | |
21 | | This value is the configuration for the cobalt driver
22 | | url: the url of the cobalt server
23 | | hls: whether to use HLS or not
24 | | auth: "Authorization" header for the cobalt server
25 | |
26 | */
27 | 'cobalt' => [
28 | 'url' => null,
29 | 'hls' => false,
30 | 'auth' => env('YOUTUBE_API_COBALT_AUTH'),
31 | ],
32 |
33 | /*
34 | |--------------------------------------------------------------------------
35 | | Routing prefix
36 | |--------------------------------------------------------------------------
37 | |
38 | | This value is the section of all the api routes after 'api/'
39 | | Example: api/ytconverter/convert
40 | |
41 | */
42 |
43 | 'route_prefix' => 'ytconverter',
44 |
45 | /*
46 | |--------------------------------------------------------------------------
47 | | Set download length limit
48 | |--------------------------------------------------------------------------
49 | |
50 | | If not null, sets a limit on the video length that users can convert
51 | | Accepts:
52 | | - null
53 | | - an anonymous function with Illuminate\Http\Request parameter and returning an integer, the max video length in seconds
54 | |
55 | | Examples:
56 | | function (Illuminate\Http\Request $request)
57 | | {
58 | | $plan = $request->user()->getCurrentPlan();
59 | |
60 | | return $plan->download_limit;
61 | | }
62 | |
63 | | function (Illuminate\Http\Request $request)
64 | | {
65 | | return 300;
66 | | }
67 | */
68 |
69 | 'videolength_limiter' => null,
70 |
71 | /*
72 | |--------------------------------------------------------------------------
73 | | Maximum search results
74 | |--------------------------------------------------------------------------
75 | |
76 | | Specify the maximum amount of results the search route returns
77 | |
78 | */
79 |
80 | 'search_max_results' => 10,
81 |
82 | /*
83 | |--------------------------------------------------------------------------
84 | | FFMPEG bin path
85 | |--------------------------------------------------------------------------
86 | |
87 | | The location of the ffmpeg executable in case when manually build in stead of yum install or apt-get install
88 | |
89 | */
90 |
91 | 'ffmpeg_path' => null,
92 |
93 | /*
94 | |--------------------------------------------------------------------------
95 | | Set authentication
96 | |--------------------------------------------------------------------------
97 | |
98 | | If not null, it'll attempt to use the defined authentication guard for api routes
99 | |
100 | */
101 |
102 | 'auth' => null,
103 |
104 | /*
105 | |--------------------------------------------------------------------------
106 | | Set ratelimiter
107 | |--------------------------------------------------------------------------
108 | |
109 | | If not null, sets a ratelimiter on the api group routes
110 | | Accepts:
111 | | - null
112 | | - an anonymous function with Illuminate\Http\Request parameter and returning an instance of Illuminate\Cache\RateLimiting\Limit
113 | |
114 | | More info: https://laravel.com/docs/8.x/routing#rate-limiting
115 | |
116 | */
117 | 'ratelimiter' => null,
118 |
119 | /*
120 | |--------------------------------------------------------------------------
121 | | Enable logging
122 | |--------------------------------------------------------------------------
123 | |
124 | | Save an entry every time a video gets converted into logs table.
125 | |
126 | | Note: this also enables the page /logs with an overview of all songs that have been converted
127 | |
128 | */
129 | 'enable_logging' => false,
130 |
131 | /*
132 | |--------------------------------------------------------------------------
133 | | Proxy Configuration
134 | |--------------------------------------------------------------------------
135 | |
136 | | If not null, sets a proxy for yt-dlp or youtube-dl.
137 | | Accepts:
138 | | - null
139 | | - a string representing the proxy URL
140 | |
141 | */
142 | 'proxy' => env('YOUTUBE_API_PROXY'),
143 | ];
--------------------------------------------------------------------------------
/database/migrations/2021_10_06_094035_create_logs_table.php:
--------------------------------------------------------------------------------
1 | id();
19 | $table->string('youtube_id', 32);
20 | $table->string('title');
21 | $table->smallInteger('duration')->unsigned();
22 | $table->string('format', 16)->default('mp3');
23 | $table->timestamp('created_at')->default(DB::raw('CURRENT_TIMESTAMP'));
24 | });
25 | }
26 |
27 | /**
28 | * Reverse the migrations.
29 | *
30 | * @return void
31 | */
32 | public function down()
33 | {
34 | Schema::dropIfExists('youtube_logs');
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/database/migrations/2021_10_31_125901_add_user_to_log.php:
--------------------------------------------------------------------------------
1 | foreignId('user_id')->nullable()->after('id');
18 |
19 | $table->foreign('user_id')->references('id')->on('users')->onDelete('SET NULL')->onUpdate('CASCADE');
20 | });
21 | }
22 |
23 | /**
24 | * Reverse the migrations.
25 | *
26 | * @return void
27 | */
28 | public function down()
29 | {
30 | Schema::table('youtube_logs', function (Blueprint $table) {
31 | $table->dropConstrainedForeignId('user_id');
32 | });
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/resources/views/home/index.blade.php:
--------------------------------------------------------------------------------
1 | @extends('youtube-api-views::template')
2 |
3 | @section('content')
4 |
5 |
6 |
7 |
8 |
9 |
12 |
13 | @if (session('error'))
14 |
{{ session('error') }}
15 | @endif
16 |
17 |
44 |
45 |
46 |
47 | @if (session('converted'))
48 |
49 |
52 |
53 |
@json(session('converted'), JSON_PRETTY_PRINT)
54 |
55 |
102 |
103 | @endif
104 |
105 |
106 |
107 |
136 |
137 | @if (session('searched'))
138 |
139 |
142 |
143 |
@json(session('searched'), JSON_PRETTY_PRINT)
144 |
145 |
178 |
179 | @endif
180 |
181 |
182 |
183 | @endsection
--------------------------------------------------------------------------------
/resources/views/logs/index.blade.php:
--------------------------------------------------------------------------------
1 | @extends('youtube-api-views::template')
2 |
3 | @section('styles')
4 |
5 | @endsection
6 |
7 | @section('content')
8 |
9 |
Logs
10 |
11 |
18 |
19 |
20 |
21 |
22 |
23 | Time |
24 | ID |
25 | Title |
26 | Duration |
27 | Format |
28 | Location |
29 |
30 |
31 |
32 | @foreach ($logs as $log)
33 |
34 | {{ $log->created_at->format('H:i:s') }} |
35 | {{ $log->youtube_id }} |
36 | {{ $log->title }} |
37 | {{ $log->duration }} seconds |
38 | {{ $log->format }} |
39 |
40 | @php
41 | $path = \MichaelBelgium\YoutubeAPI\Models\Video::getDownloadPath($log->youtube_id.".".$log->format);
42 | @endphp
43 | @if (File::exists($path))
44 | @php
45 | $url = \MichaelBelgium\YoutubeAPI\Models\Video::getDownloadUrl($log->youtube_id.".".$log->format);
46 | @endphp
47 | Converted file
48 | @else
49 | Removed
50 | @endif
51 | |
52 |
53 | @endforeach
54 |
55 |
56 |
57 | {{ $logs->links() }}
58 |
59 | @endsection
--------------------------------------------------------------------------------
/resources/views/template.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Youtube converter
8 |
9 |
10 | @yield('styles')
11 |
12 |
13 |
30 |
31 | @yield('content')
32 |
33 |
34 | @yield('scripts')
35 |
36 |
--------------------------------------------------------------------------------
/routes/api.php:
--------------------------------------------------------------------------------
1 | name('youtube-api.convert');
6 | Route::get('/search', [ApiController::class, 'search'])->name('youtube-api.search');
7 | Route::get('/info', [ApiController::class, 'info'])->name('youtube-api.info');
8 | Route::delete('/{id}', [ApiController::class, 'delete'])->name('youtube-api.delete');
--------------------------------------------------------------------------------
/routes/web.php:
--------------------------------------------------------------------------------
1 | name('youtube-api.index');
7 | Route::post('/', [HomeController::class, 'onPost'])->name('youtube-api.submit');
8 | Route::get('/logs', [HomeController::class, 'logs'])->name('youtube-api.logs');
--------------------------------------------------------------------------------
/src/Controllers/ApiController.php:
--------------------------------------------------------------------------------
1 | all(), [
27 | 'url' => ['required', 'string', 'url', 'regex:' . Video::URL_REGEX],
28 | 'format' => [Rule::in(Video::POSSIBLE_FORMATS)]
29 | ]);
30 |
31 | if($validator->fails()) {
32 | return response()->json(['error' => true, 'error_messages' => $validator->errors()], Response::HTTP_BAD_REQUEST);
33 | }
34 |
35 | $validated = $validator->validated();
36 |
37 | $url = Arr::get($validated, 'url');
38 | $format = Arr::get($validated, 'format', 'mp3');
39 |
40 | $lengthLimiter = config('youtube-api.videolength_limiter');
41 | $selectedDriver = config('youtube-api.driver', 'local');
42 |
43 | if($selectedDriver == 'local') {
44 | $driver = new Local($url, $format);
45 | } else if ($selectedDriver == 'cobalt') {
46 | $driver = new Cobalt($url, $format);
47 | } else {
48 | return response()->json(['error' => true, 'message' => 'Invalid driver'], Response::HTTP_BAD_REQUEST);
49 | }
50 |
51 | try
52 | {
53 | $video = $driver->convert();
54 |
55 | if($lengthLimiter !== null && is_callable($lengthLimiter))
56 | {
57 | $maxLength = $lengthLimiter($request);
58 |
59 | if($video->getDuration() > $maxLength && $maxLength > 0)
60 | throw new Exception("The duration of the video is {$video->getDuration()} seconds while max video length is $maxLength seconds.");
61 | }
62 |
63 | if(config('youtube-api.enable_logging', false) === true)
64 | {
65 | $log = new Log();
66 | $log->youtube_id = $video->getId();
67 | $log->title = $video->getTitle();
68 | $log->duration = $video->getDuration();
69 | $log->format = $format;
70 |
71 | if(config('youtube-api.auth') !== null)
72 | $log->user()->associate(Auth::user());
73 |
74 | $log->save();
75 | }
76 |
77 | return response()->json([
78 | 'error' => false,
79 | ...$video->toArray()
80 | ]);
81 | }
82 | catch (Exception $e)
83 | {
84 | return response()->json(['error' => true, 'message' => $e->getMessage()], Response::HTTP_BAD_REQUEST);
85 | }
86 | }
87 |
88 | public function delete(Request $request, string $id)
89 | {
90 | $removedFiles = [];
91 |
92 | foreach(Video::POSSIBLE_FORMATS as $format) {
93 | $localFile = Video::getDownloadPath("$id.$format");
94 |
95 | if(File::exists($localFile)) {
96 | File::delete($localFile);
97 | $removedFiles[] = $format;
98 | }
99 | }
100 |
101 | $resultNotRemoved = array_diff(Video::POSSIBLE_FORMATS, $removedFiles);
102 |
103 | if(empty($removedFiles))
104 | $message = 'No files removed.';
105 | else
106 | $message = 'Removed files: ' . implode(', ', $removedFiles) . '.';
107 |
108 | if(!empty($resultNotRemoved))
109 | $message .= ' Not removed: ' . implode(', ', $resultNotRemoved);
110 |
111 | return response()->json(['error' => false, 'message' => $message]);
112 | }
113 |
114 | public function info(Request $request)
115 | {
116 | if(empty(env('GOOGLE_API_KEY'))) {
117 | return response()->json(['error' => true, 'message' => 'No google api key specified'], Response::HTTP_BAD_REQUEST);
118 | }
119 |
120 | $query = $request->get('query', $request->get('q'));
121 |
122 | if(empty($query)) {
123 | return response()->json(['error' => true, 'message' => 'No query specified'], Response::HTTP_BAD_REQUEST);
124 | }
125 |
126 | if (filter_var($query, FILTER_VALIDATE_URL))
127 | $id = Video::getVideoId($query);
128 | else
129 | $id = $query;
130 |
131 | $gClient = new \Google_Client();
132 | $gClient->setDeveloperKey(env('GOOGLE_API_KEY'));
133 |
134 | $youtube = new \Google_Service_YouTube($gClient);
135 | $response = $youtube->videos->listVideos('snippet,contentDetails', ['id' => $id]);
136 | $ytVideo = $response->getItems()[0] ?? null;
137 |
138 | if ($ytVideo == null) {
139 | return response()->json(['error' => true, 'message' => 'Video not found'], Response::HTTP_NOT_FOUND);
140 | }
141 |
142 | $duration = $ytVideo->getContentDetails()->getDuration();
143 | $interval = new \DateInterval($duration);
144 | $duration = ($interval->h * 60 * 60) + ($interval->i * 60) + $interval->s;
145 |
146 | return response()->json([
147 | 'error' => false,
148 | 'id' => $id,
149 | 'url' => 'https://youtube.com/watch?v='.$id,
150 | 'channel_url' => 'https://youtube.com/channel/'.$ytVideo->getSnippet()->getChannelId(),
151 | 'channel_id' => $ytVideo->getSnippet()->getChannelId(),
152 | 'channel' => $ytVideo->getSnippet()->getChannelTitle(),
153 | 'title' => $ytVideo->getSnippet()->getTitle(),
154 | 'duration' => $duration,
155 | 'description' => $ytVideo->getSnippet()->getDescription(),
156 | 'published_at' => $ytVideo->getSnippet()->getPublishedAt()
157 | ]);
158 | }
159 |
160 | public function search(Request $request)
161 | {
162 | if(empty(env('GOOGLE_API_KEY'))) {
163 | return response()->json(['error' => true, 'message' => 'No google api key specified'], Response::HTTP_BAD_REQUEST);
164 | }
165 |
166 | $validator = Validator::make($request->all(), [
167 | 'q' => ['required', 'string'],
168 | 'max_results' => ['numeric']
169 | ]);
170 |
171 |
172 | if($validator->fails()) {
173 | return response()->json(['error' => true, 'error_messages' => $validator->errors()], Response::HTTP_BAD_REQUEST);
174 | }
175 |
176 | $data = $validator->validated();
177 |
178 | $q = $data['q'];
179 | $max_results = $data['max_results'] ?? config('youtube-api.search_max_results', 10);
180 |
181 | $gClient = new Google_Client();
182 | $gClient->setDeveloperKey(env('GOOGLE_API_KEY'));
183 |
184 | $guzzleClient = new Client([
185 | RequestOptions::HEADERS => [
186 | 'referer' => env('APP_URL')
187 | ]
188 | ]);
189 |
190 | $gClient->setHttpClient($guzzleClient);
191 |
192 | $ytService = new Google_Service_YouTube($gClient);
193 |
194 | try {
195 | $search = $ytService->search->listSearch('id,snippet', [
196 | 'q' => $q,
197 | 'maxResults' => $max_results,
198 | 'type' => 'video'
199 | ]);
200 |
201 | $results = [];
202 |
203 | foreach ($search['items'] as $searchResult)
204 | {
205 | $results[] = array(
206 | 'id' => $searchResult['id']['videoId'],
207 | 'channel' => $searchResult['snippet']['channelTitle'],
208 | 'title' => $searchResult['snippet']['title'],
209 | 'full_link' => 'https://youtube.com/watch?v='.$searchResult['id']['videoId']
210 | );
211 | }
212 |
213 | return response()->json(['error' => false, 'message' => '', 'results' => $results]);
214 |
215 | } catch (Exception $ex) {
216 | $errorObj = json_decode($ex->getMessage());
217 | return response()->json(['error' => true, 'message' => $errorObj->error->message], Response::HTTP_BAD_REQUEST);
218 | }
219 | }
220 |
221 |
222 | }
223 |
--------------------------------------------------------------------------------
/src/Controllers/HomeController.php:
--------------------------------------------------------------------------------
1 | has('q'))
22 | $response = Http::withToken($request->token)->get(route('youtube-api.search', $request->all()));
23 | else
24 | $response = Http::withToken($request->token)->post(route('youtube-api.convert'), $request->all());
25 |
26 | if($response->status() == Response::HTTP_UNAUTHORIZED || $response->status() == Response::HTTP_TOO_MANY_REQUESTS)
27 | return back()->with('error', $response->object()->message);
28 | else if($response->status() == Response::HTTP_BAD_REQUEST) {
29 | $errObj = $response->object();
30 |
31 | if(property_exists($errObj, 'error_messages'))
32 | return redirect()->back()->withErrors($errObj->error_messages);
33 |
34 | return back()->with('error', $errObj->message);
35 | }
36 |
37 | return redirect()->back()->with($request->has('q') ? 'searched' : 'converted', $response->object());
38 | }
39 |
40 | public function logs(Request $request)
41 | {
42 | abort_if(config('youtube-api.enable_logging', false) === false, Response::HTTP_NOT_FOUND);
43 |
44 | $selectedDate = $request->get('date');
45 |
46 | /** @var Collection $dates */
47 | $dates = Log::select(DB::raw('DATE(created_at) date'))
48 | ->groupBy(DB::raw('DATE(created_at)'))
49 | ->orderBy(DB::raw('DATE(created_at)'), 'DESC')
50 | ->get();
51 |
52 | $dates = $dates->map(fn($date) => $date->date);
53 |
54 | $logs = Log::where(DB::raw('DATE(created_at)'), $request->get('date', $dates->first()))
55 | ->orderBy('created_at', 'DESC')
56 | ->simplePaginate(25)
57 | ->withQueryString();
58 |
59 | return view('youtube-api-views::logs.index', [
60 | 'dates' => $dates,
61 | 'logs' => $logs,
62 | 'selectedDate' => $selectedDate,
63 | ]);
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/Drivers/Cobalt.php:
--------------------------------------------------------------------------------
1 | url = $url;
18 | $this->format = $format;
19 | }
20 |
21 | public function convert(): Video
22 | {
23 | if (config('youtube-api.cobalt.url') === null)
24 | throw new Exception('Cobalt API url is not configured');
25 |
26 | $request = Http::accept('application/json');
27 |
28 | if (config('youtube-api.cobalt.auth') !== null)
29 | $request->withHeaders(['Authorization' => config('youtube-api.cobalt.auth')]);
30 |
31 | $response = $request->post(config('youtube-api.cobalt.url'), [
32 | 'url' => $this->url,
33 | 'downloadMode' => $this->format == 'mp3' ? 'audio' : 'auto',
34 | 'youtubeHLS' => config('youtube-api.cobalt.hls', false)
35 | ]);
36 |
37 | if ($response->status() != 200)
38 | throw new Exception($response->body());
39 |
40 | $cobalt = $response->object();
41 |
42 | if ($cobalt->status != 'tunnel')
43 | throw new Exception('This status is not supported yet');
44 |
45 | $id = Video::getVideoId($this->url);
46 |
47 | if (env('GOOGLE_API_KEY') === null)
48 | {
49 | $file = explode('_', $cobalt->filename);
50 | $cobalt->filename = $file[1] . '.' . $this->format;
51 |
52 | if (!Storage::disk('public')->exists($cobalt->filename))
53 | {
54 | $success = Storage::disk('public')
55 | ->put($cobalt->filename, file_get_contents($cobalt->url));
56 |
57 | if (!$success)
58 | throw new Exception('Failed to save file');
59 | }
60 |
61 | $url = Video::getDownloadUrl($cobalt->filename);
62 | }
63 | else
64 | {
65 | $url = $cobalt->url;
66 | }
67 |
68 | [$title, $duration, $uploadedAt] = $this->getMetaData();
69 |
70 | $video = new Video(
71 | $id,
72 | $title,
73 | $url
74 | );
75 |
76 | $video->setUploadedAt($uploadedAt);
77 | $video->setDuration($duration);
78 |
79 | return $video;
80 | }
81 |
82 | /**
83 | * @todo use some kind of cache, to prevent multiple requests or analyzes. The chances of the video changing title or (for sure) duration are ultra low
84 | */
85 | private function getMetaData(): array
86 | {
87 | $id = Video::getVideoId($this->url);
88 |
89 | if (env('GOOGLE_API_KEY') === null)
90 | {
91 | $getID3 = new \getID3();
92 | $fileInfo = $getID3->analyze(Video::getDownloadPath($id . '.' . $this->format));
93 |
94 | return [
95 | $fileInfo['tags'][$this->format == 'mp3' ? 'id3v2' : 'quicktime']['title'][0],
96 | $fileInfo['playtime_seconds'],
97 | null
98 | ];
99 | }
100 | else
101 | {
102 | //todo same as /info endpoint
103 | $gClient = new \Google_Client();
104 | $gClient->setDeveloperKey(env('GOOGLE_API_KEY'));
105 |
106 | $youtube = new \Google_Service_YouTube($gClient);
107 | $response = $youtube->videos->listVideos('snippet,contentDetails', ['id' => $id]);
108 | $ytVideo = $response->getItems()[0] ?? null;
109 |
110 | if ($ytVideo == null)
111 | throw new Exception('Video not found');
112 |
113 | $duration = $ytVideo->getContentDetails()->getDuration();
114 | $interval = new \DateInterval($duration);
115 | $duration = ($interval->h * 60 * 60) + ($interval->i * 60) + $interval->s;
116 |
117 | return [
118 | $ytVideo->getSnippet()->getTitle(),
119 | $duration,
120 | new \DateTime($ytVideo->getSnippet()->getPublishedAt())
121 | ];
122 | }
123 | }
124 | }
--------------------------------------------------------------------------------
/src/Drivers/IDriver.php:
--------------------------------------------------------------------------------
1 | youtubeDl = new YoutubeDl();
20 | $this->format = $format;
21 |
22 | $this->options = Options::create()
23 | ->noPlaylist()
24 | ->downloadPath(Video::getDownloadPath())
25 | ->proxy(config('youtube-api.proxy'))
26 | ->url($url);
27 | }
28 |
29 | public function convert(): Video
30 | {
31 | $options = $this->options->output('%(id)s.%(ext)s');
32 |
33 | if($this->format == 'mp3')
34 | {
35 | $options = $options->extractAudio(true)
36 | ->audioFormat('mp3')
37 | ->audioQuality('0');
38 |
39 | if(config('youtube-api.ffmpeg_path') !== null) {
40 | $options = $options->ffmpegLocation(config('youtube-api.ffmpeg_path'));
41 | }
42 | }
43 | else
44 | $options = $options->format('bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best');
45 |
46 | $id = Video::getVideoId($this->options->getUrl()[0]);
47 |
48 | if (Storage::disk('public')->exists($id . '.' . $this->format))
49 | $ytdlVideo = $this->getVideoWithoutDownload();
50 | else
51 | $ytdlVideo = $this->youtubeDl->download($options)->getVideos()[0];
52 |
53 | if ($ytdlVideo->getError() !== null)
54 | throw new Exception($ytdlVideo->getError());
55 |
56 | $video = new Video(
57 | $ytdlVideo->getId(),
58 | $ytdlVideo->getTitle(),
59 | Video::getDownloadUrl($ytdlVideo->getId() . '.' . $this->format)
60 | );
61 |
62 | $video->setUploadedAt($ytdlVideo->getUploadDate());
63 | $video->setDuration($ytdlVideo->getDuration());
64 |
65 | return $video;
66 | }
67 |
68 | public function getVideoWithoutDownload(): \YoutubeDl\Entity\Video
69 | {
70 | //todo kinda same as /info endpoint
71 | $ytdlVideo = $this->youtubeDl->download(
72 | $this->options->skipDownload(true)
73 | )->getVideos()[0];
74 |
75 | return $ytdlVideo;
76 | }
77 | }
--------------------------------------------------------------------------------
/src/Models/Log.php:
--------------------------------------------------------------------------------
1 | belongsTo(config('auth.providers.users.model'));
20 | else
21 | throw new Exception('No user provider model defined.');
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/Models/Video.php:
--------------------------------------------------------------------------------
1 | id = $id;
24 | $this->title = $title;
25 | $this->file = $file;
26 | }
27 |
28 | public function getId(): string
29 | {
30 | return $this->id;
31 | }
32 |
33 | public function getTitle(): string
34 | {
35 | return $this->title;
36 | }
37 |
38 | public function getFile(): string
39 | {
40 | return $this->file;
41 | }
42 |
43 | public function getUploadedAt(): ?\DateTimeInterface
44 | {
45 | return $this->uploadedAt;
46 | }
47 |
48 | public function getDuration(): float
49 | {
50 | return $this->duration;
51 | }
52 |
53 | public function setUploadedAt(?\DateTimeInterface $uploadedAt): self
54 | {
55 | $this->uploadedAt = $uploadedAt;
56 |
57 | return $this;
58 | }
59 |
60 | public function setDuration(float $duration): self
61 | {
62 | $this->duration = $duration;
63 |
64 | return $this;
65 | }
66 |
67 | public static function getDownloadPath(string $file = ''): string
68 | {
69 | return Storage::disk('public')->path($file);
70 | }
71 |
72 | public static function getDownloadUrl(string $file = ''): string
73 | {
74 | return Storage::disk('public')->url($file);
75 | }
76 |
77 | public static function getVideoId(string $url): string
78 | {
79 | preg_match(static::URL_REGEX, $url, $matches);
80 | return $matches[0];
81 | }
82 |
83 | public function toArray(): array
84 | {
85 | return [
86 | 'youtube_id' => $this->getId(),
87 | 'title' => $this->getTitle(),
88 | 'file' => $this->getFile(),
89 | 'uploaded_at' => $this->getUploadedAt(),
90 | 'duration' => $this->getDuration()
91 | ];
92 | }
93 | }
--------------------------------------------------------------------------------
/src/YoutubeAPIServiceProvider.php:
--------------------------------------------------------------------------------
1 | app->runningInConsole()) {
18 | $this->publishes([
19 | __DIR__ . '/../config/youtube-api.php' => config_path('youtube-api.php')
20 | ], 'youtube-api-config');
21 | }
22 |
23 | $this->loadViewsFrom(__DIR__.'/../resources/views', 'youtube-api-views');
24 | $this->loadRoutes();
25 | $this->loadMigrationsFrom(__DIR__.'/../database/migrations');
26 | }
27 |
28 | /**
29 | * Register the application services.
30 | */
31 | public function register()
32 | {
33 | $this->mergeConfigFrom(__DIR__.'/../config/youtube-api.php', 'laravel-youtube-api');
34 | }
35 |
36 | private function loadRoutes() {
37 | $apiMiddleware = ['api'];
38 |
39 | if(config('youtube-api.auth') !== null) {
40 | $apiMiddleware[] = 'auth:' . config('youtube-api.auth');
41 | }
42 |
43 | if(config('youtube-api.ratelimiter') !== null) {
44 | RateLimiter::for('yt-api', config('youtube-api.ratelimiter'));
45 |
46 | $apiMiddleware[] = 'throttle:yt-api';
47 | }
48 |
49 | Route::prefix('api/' . config('youtube-api.route_prefix', 'ytconverter'))
50 | ->middleware($apiMiddleware)
51 | ->namespace( 'MichaelBelgium\YoutubeAPI\Controllers')
52 | ->group(__DIR__.'/../routes/api.php');
53 |
54 | Route::prefix(config('youtube-api.route_prefix', 'ytconverter'))
55 | ->middleware('web')
56 | ->namespace('MichaelBelgium\YoutubeAPI\Controllers')
57 | ->group(__DIR__.'/../routes/web.php');
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/tests/ApiTest.php:
--------------------------------------------------------------------------------
1 | post('/ytconverter/convert');
17 | $response->assertSuccessful();
18 | }
19 | }
20 |
--------------------------------------------------------------------------------