diff --git a/.blueprint b/.blueprint
index 891623c0..14741d32 100644
--- a/.blueprint
+++ b/.blueprint
@@ -1,16 +1,17 @@
-created: 'database/factories/LibraryFactory.php database/factories/LibraryItemsFactory.php database/migrations/2022_12_05_160932_create_libraries_table.php database/migrations/2022_12_05_160933_create_library_items_table.php app/Models/Library.php app/Models/LibraryItems.php app/Nova/Library.php app/Nova/LibraryItems.php'
models:
Category: { name: string, slug: string }
City: { country_id: biginteger, name: string, slug: string, longitude: 'float:10', latitude: 'float:10' }
- Country: { name: string, code: string }
+ Country: { name: string, code: string, language_codes: 'json default:[]' }
Course: { lecturer_id: biginteger, name: string, description: 'text nullable' }
+ Episode: { guid: string, podcast_id: biginteger, data: json }
Event: { course_id: biginteger, venue_id: biginteger, '"from"': datetime, '"to"': datetime, link: string }
Lecturer: { team_id: biginteger, name: string, slug: string, active: 'boolean default:1', description: 'text nullable' }
- Library: { name: string, language_code: string }
- LibraryItem: { lecturer_id: biginteger, library_id: biginteger, order_column: integer, type: string, value: text }
+ Library: { name: string, is_public: 'boolean default:1', language_codes: 'json default:[]' }
+ LibraryItem: { lecturer_id: biginteger, episode_id: 'biginteger nullable', order_column: integer, name: string, type: string, language_code: string, value: 'text nullable' }
LoginKey: { k1: string, user_id: biginteger }
Membership: { team_id: biginteger, user_id: biginteger, role: 'string nullable' }
Participant: { first_name: string, last_name: string }
+ Podcast: { guid: string, title: string, link: string, language_code: string, data: json }
Registration: { event_id: biginteger, participant_id: biginteger, active: 'boolean default:1' }
Tag: { name: json, slug: json, type: 'string nullable', order_column: 'integer nullable', icon: 'string default:tag' }
Team: { user_id: biginteger, name: string, personal_team: boolean }
diff --git a/app/Console/Commands/Feed/ReadAndSyncEinundzwanzigPodcastFeed.php b/app/Console/Commands/Feed/ReadAndSyncEinundzwanzigPodcastFeed.php
new file mode 100644
index 00000000..7f71eaf4
--- /dev/null
+++ b/app/Console/Commands/Feed/ReadAndSyncEinundzwanzigPodcastFeed.php
@@ -0,0 +1,55 @@
+ 'Einundzwanzig School',
+ 'key' => config('feeds.services.podcastindex-org.key'),
+ 'secret' => config('feeds.services.podcastindex-org.secret'),
+ ]);
+ $podcast = $client->podcasts->byFeedUrl('https://einundzwanzig.space/feed.xml')
+ ->json();
+ $einundzwanzigPodcast = Podcast::query()
+ ->updateOrCreate(['guid' => $podcast->feed->podcastGuid], [
+ 'title' => $podcast->feed->title,
+ 'link' => $podcast->feed->link,
+ 'language_code' => $podcast->feed->language,
+ 'data' => $podcast->feed,
+ ]);
+ $episodes = $client->episodes->byFeedUrl('https://einundzwanzig.space/feed.xml')
+ ->json();
+ foreach ($episodes->items as $item) {
+ Episode::query()
+ ->updateOrCreate(['guid' => $item->guid], [
+ 'podcast_id' => $einundzwanzigPodcast->id,
+ 'data' => $item,
+ ]);
+ }
+
+ return Command::SUCCESS;
+ }
+}
diff --git a/app/Http/Livewire/Frontend/Library.php b/app/Http/Livewire/Frontend/Library.php
index 796daee8..f0833e4a 100644
--- a/app/Http/Livewire/Frontend/Library.php
+++ b/app/Http/Livewire/Frontend/Library.php
@@ -3,6 +3,7 @@
namespace App\Http\Livewire\Frontend;
use App\Models\Country;
+use App\Models\Podcast;
use Livewire\Component;
class Library extends Component
@@ -24,13 +25,22 @@ class Library extends Component
abort(403);
}
+ $libraries = \App\Models\Library::query()
+ ->where('is_public', $shouldBePublic)
+ ->get();
+ $tabs = collect([
+ [
+ 'name' => 'Alle',
+ ]
+ ]);
+ foreach ($libraries as $library) {
+ $tabs->push([
+ 'name' => $library->name,
+ ]);
+ }
+
return view('livewire.frontend.library', [
- 'libraries' => \App\Models\Library::query()
- ->where('is_public', $shouldBePublic)
- ->get()
- ->prepend(\App\Models\Library::make([
- 'name' => 'Alle',
- ])),
+ 'libraries' => $tabs,
]);
}
}
diff --git a/app/Http/Livewire/Tables/LibraryItemTable.php b/app/Http/Livewire/Tables/LibraryItemTable.php
index 0a62b121..bd45a480 100644
--- a/app/Http/Livewire/Tables/LibraryItemTable.php
+++ b/app/Http/Livewire/Tables/LibraryItemTable.php
@@ -93,7 +93,7 @@ class LibraryItemTable extends DataTableComponent
'alt' => $row->name.' Avatar',
])
->collapseOnMobile(),
- Column::make('Dozent', "lecturer.name")
+ Column::make('Ersteller', "lecturer.name")
->label(
fn($row, Column $column) => view('columns.courses.lecturer')->withRow($row)
)
diff --git a/app/Models/Episode.php b/app/Models/Episode.php
new file mode 100644
index 00000000..4b62d5e7
--- /dev/null
+++ b/app/Models/Episode.php
@@ -0,0 +1,40 @@
+ 'integer',
+ 'podcast_id' => 'integer',
+ 'data' => 'array',
+ ];
+
+ public function podcast(): \Illuminate\Database\Eloquent\Relations\BelongsTo
+ {
+ return $this->belongsTo(Podcast::class);
+ }
+
+ public function libraryItem(): HasOne
+ {
+ return $this->hasOne(LibraryItem::class);
+ }
+}
diff --git a/app/Models/LibraryItem.php b/app/Models/LibraryItem.php
index bf513037..7a813afa 100644
--- a/app/Models/LibraryItem.php
+++ b/app/Models/LibraryItem.php
@@ -64,6 +64,11 @@ class LibraryItem extends Model implements HasMedia, Sortable
return $this->belongsTo(Lecturer::class);
}
+ public function episode(): \Illuminate\Database\Eloquent\Relations\BelongsTo
+ {
+ return $this->belongsTo(Episode::class);
+ }
+
public function libraries(): \Illuminate\Database\Eloquent\Relations\BelongsToMany
{
return $this->belongsToMany(Library::class);
diff --git a/app/Models/Podcast.php b/app/Models/Podcast.php
new file mode 100644
index 00000000..c6e8c506
--- /dev/null
+++ b/app/Models/Podcast.php
@@ -0,0 +1,32 @@
+ 'integer',
+ 'data' => 'array',
+ ];
+
+ public function episodes(): HasMany
+ {
+ return $this->hasMany(Episode::class);
+ }
+}
diff --git a/app/Models/Tag.php b/app/Models/Tag.php
index 42bed46f..bbbc63cb 100644
--- a/app/Models/Tag.php
+++ b/app/Models/Tag.php
@@ -13,4 +13,9 @@ class Tag extends \Spatie\Tags\Tag
{
return $this->morphedByMany(LibraryItem::class, 'taggable');
}
+
+ public function episodes()
+ {
+ return $this->morphedByMany(Episode::class, 'taggable');
+ }
}
diff --git a/app/Nova/Episode.php b/app/Nova/Episode.php
new file mode 100644
index 00000000..dada5eb0
--- /dev/null
+++ b/app/Nova/Episode.php
@@ -0,0 +1,156 @@
+tags) {
+ $lecturer = \App\Models\Lecturer::updateOrCreate(['name' => $model->podcast->title], [
+ 'team_id' => 1,
+ 'active' => true,
+ ]);
+ $lecturer->addMediaFromUrl($model->podcast->data['image'])
+ ->toMediaCollection('avatar');
+ $library = \App\Models\Library::updateOrCreate(
+ [
+ 'name' => $model->podcast->title
+ ],
+ [
+ 'language_codes' => [$model->podcast->language_code],
+ ]);
+ $libraryItem = $model->libraryItem()
+ ->firstOrCreate([
+ 'lecturer_id' => $lecturer->id,
+ 'episode_id' => $model->id,
+ 'name' => $model->data['title'],
+ 'type' => 'podcast_episode',
+ 'language_code' => $model->podcast->language_code,
+ 'value' => null,
+ ]);
+ ray($request->tags);
+ $libraryItem->syncTagsWithType(is_array($request->tags) ? $request->tags : str($request->tags)->explode('-----'),
+ 'library_item');
+ $libraryItem->addMediaFromUrl($model->data['image'])
+ ->toMediaCollection('main');
+ $library->libraryItems()
+ ->attach($libraryItem);
+ }
+ }
+
+ public function title()
+ {
+ return $this->data['title'];
+ }
+
+ /**
+ * Get the fields displayed by the resource.
+ *
+ * @param \Illuminate\Http\Request $request
+ *
+ * @return array
+ */
+ public function fields(Request $request)
+ {
+ return [
+ ID::make()
+ ->sortable(),
+
+ Avatar::make('Image')
+ ->squared()
+ ->thumbnail(function () {
+ return $this->data['image'];
+ })
+ ->exceptOnForms(),
+
+ Tags::make('Tags')
+ ->type('library_item')
+ ->withLinkToTagResource(Tag::class),
+
+ Text::make('Title', 'data->title')
+ ->readonly()
+ ->rules('required', 'string'),
+
+ Code::make('Data')
+ ->readonly()
+ ->rules('required', 'json')
+ ->json(),
+
+ BelongsTo::make('Podcast')
+ ->readonly(),
+
+ ];
+ }
+
+ /**
+ * Get the cards available for the request.
+ *
+ * @param \Illuminate\Http\Request $request
+ *
+ * @return array
+ */
+ public function cards(Request $request)
+ {
+ return [];
+ }
+
+ /**
+ * Get the filters available for the resource.
+ *
+ * @param \Illuminate\Http\Request $request
+ *
+ * @return array
+ */
+ public function filters(Request $request)
+ {
+ return [];
+ }
+
+ /**
+ * Get the lenses available for the resource.
+ *
+ * @param \Illuminate\Http\Request $request
+ *
+ * @return array
+ */
+ public function lenses(Request $request)
+ {
+ return [];
+ }
+
+ /**
+ * Get the actions available for the resource.
+ *
+ * @param \Illuminate\Http\Request $request
+ *
+ * @return array
+ */
+ public function actions(Request $request)
+ {
+ return [];
+ }
+}
diff --git a/app/Nova/LibraryItem.php b/app/Nova/LibraryItem.php
index 692d06dc..27bfda1b 100644
--- a/app/Nova/LibraryItem.php
+++ b/app/Nova/LibraryItem.php
@@ -80,6 +80,7 @@ class LibraryItem extends Resource
'markdown_article' => 'markdown_article',
'youtube_video' => 'youtube_video',
'vimeo_video' => 'vimeo_video',
+ 'podcast_episode' => 'podcast_episode',
'downloadable_file' => 'downloadable_file',
]
)
@@ -91,6 +92,8 @@ class LibraryItem extends Resource
BelongsTo::make('Lecturer'),
+ BelongsTo::make('Episode'),
+
BelongsToMany::make('Library', 'libraries', Library::class),
];
diff --git a/app/Nova/Podcast.php b/app/Nova/Podcast.php
new file mode 100644
index 00000000..8ae8875b
--- /dev/null
+++ b/app/Nova/Podcast.php
@@ -0,0 +1,118 @@
+sortable(),
+
+ Avatar::make('Image')
+ ->squared()
+ ->thumbnail(function () {
+ return $this->data['image'];
+ }),
+
+ Text::make('Title')
+ ->rules('required', 'string'),
+
+ Text::make('Language Code')
+ ->rules('required', 'string'),
+
+ Text::make('Link')
+ ->rules('required', 'string'),
+
+ Code::make('Data')
+ ->rules('required', 'json')
+ ->json(),
+
+ HasMany::make('Episodes'),
+ ];
+ }
+
+ /**
+ * Get the cards available for the request.
+ *
+ * @param \Illuminate\Http\Request $request
+ *
+ * @return array
+ */
+ public function cards(Request $request)
+ {
+ return [];
+ }
+
+ /**
+ * Get the filters available for the resource.
+ *
+ * @param \Illuminate\Http\Request $request
+ *
+ * @return array
+ */
+ public function filters(Request $request)
+ {
+ return [];
+ }
+
+ /**
+ * Get the lenses available for the resource.
+ *
+ * @param \Illuminate\Http\Request $request
+ *
+ * @return array
+ */
+ public function lenses(Request $request)
+ {
+ return [];
+ }
+
+ /**
+ * Get the actions available for the resource.
+ *
+ * @param \Illuminate\Http\Request $request
+ *
+ * @return array
+ */
+ public function actions(Request $request)
+ {
+ return [];
+ }
+}
diff --git a/app/Observers/EpisodeObserver.php b/app/Observers/EpisodeObserver.php
new file mode 100644
index 00000000..0b5f8e72
--- /dev/null
+++ b/app/Observers/EpisodeObserver.php
@@ -0,0 +1,68 @@
+hasRole('super-admin');
+ }
+
+ /**
+ * Determine whether the user can delete the model.
+ *
+ * @param \App\Models\User $user
+ * @param \App\Models\Episode $episode
+ *
+ * @return \Illuminate\Auth\Access\Response|bool
+ */
+ public function delete(User $user, Episode $episode)
+ {
+ return false;
+ }
+
+ /**
+ * Determine whether the user can restore the model.
+ *
+ * @param \App\Models\User $user
+ * @param \App\Models\Episode $episode
+ *
+ * @return \Illuminate\Auth\Access\Response|bool
+ */
+ public function restore(User $user, Episode $episode)
+ {
+ return false;
+ }
+
+ /**
+ * Determine whether the user can permanently delete the model.
+ *
+ * @param \App\Models\User $user
+ * @param \App\Models\Episode $episode
+ *
+ * @return \Illuminate\Auth\Access\Response|bool
+ */
+ public function forceDelete(User $user, Episode $episode)
+ {
+ return false;
+ }
+}
diff --git a/app/Policies/PodcastPolicy.php b/app/Policies/PodcastPolicy.php
new file mode 100644
index 00000000..cbe691b7
--- /dev/null
+++ b/app/Policies/PodcastPolicy.php
@@ -0,0 +1,94 @@
+>
*/
protected $listen = [
@@ -23,17 +21,15 @@ class EventServiceProvider extends ServiceProvider
/**
* Register any events for your application.
- *
* @return void
*/
public function boot()
{
- \App\Models\Event::observe(EventObserver::class);
+ //
}
/**
* Determine if events and listeners should be automatically discovered.
- *
* @return bool
*/
public function shouldDiscoverEvents()
diff --git a/app/Providers/NovaServiceProvider.php b/app/Providers/NovaServiceProvider.php
index d1eb5ef8..b7ecbeb4 100644
--- a/app/Providers/NovaServiceProvider.php
+++ b/app/Providers/NovaServiceProvider.php
@@ -7,11 +7,13 @@ use App\Nova\City;
use App\Nova\Country;
use App\Nova\Course;
use App\Nova\Dashboards\Main;
+use App\Nova\Episode;
use App\Nova\Event;
use App\Nova\Lecturer;
use App\Nova\Library;
use App\Nova\LibraryItem;
use App\Nova\Participant;
+use App\Nova\Podcast;
use App\Nova\Registration;
use App\Nova\Tag;
use App\Nova\Team;
@@ -59,6 +61,13 @@ class NovaServiceProvider extends NovaApplicationServiceProvider
->icon('library')
->collapsable(),
+ MenuSection::make('Podcasts', [
+ MenuItem::resource(Podcast::class),
+ MenuItem::resource(Episode::class),
+ ])
+ ->icon('microphone')
+ ->collapsable(),
+
MenuSection::make('Admin', [
MenuItem::resource(Category::class),
MenuItem::resource(Country::class),
diff --git a/composer.json b/composer.json
index b23136a2..83212a3b 100644
--- a/composer.json
+++ b/composer.json
@@ -22,6 +22,7 @@
"laravel/tinker": "^2.7",
"livewire/livewire": "^2.5",
"nova/start": "*",
+ "podcastindex/podcastindex-php": "^1.0",
"rappasoft/laravel-livewire-tables": "^2.8",
"sentry/sentry-laravel": "^3.1",
"simplesoftwareio/simple-qrcode": "^4.2",
diff --git a/composer.lock b/composer.lock
index 88d4fb99..f33b9562 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "fe13b4c19e33aebc3926e4dcd5ddf8e8",
+ "content-hash": "9c4898684e578e46bb709340edf618e8",
"packages": [
{
"name": "akuechler/laravel-geoly",
@@ -4941,6 +4941,37 @@
},
"time": "2021-10-28T11:13:42+00:00"
},
+ {
+ "name": "podcastindex/podcastindex-php",
+ "version": "1.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/LowSociety/podcastindex-php.git",
+ "reference": "8aa323cf67e1892c8ebec4500803fb8bf14ba84b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/LowSociety/podcastindex-php/zipball/8aa323cf67e1892c8ebec4500803fb8bf14ba84b",
+ "reference": "8aa323cf67e1892c8ebec4500803fb8bf14ba84b",
+ "shasum": ""
+ },
+ "require": {
+ "guzzlehttp/guzzle": "^7.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "PodcastIndex\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "description": "A PHP wrapper for the PodcastIndex API.",
+ "support": {
+ "issues": "https://github.com/LowSociety/podcastindex-php/issues",
+ "source": "https://github.com/LowSociety/podcastindex-php/tree/1.0.0"
+ },
+ "time": "2020-09-10T10:09:37+00:00"
+ },
{
"name": "pragmarx/google2fa",
"version": "v8.0.1",
diff --git a/config/feeds/services.php b/config/feeds/services.php
new file mode 100644
index 00000000..287878bd
--- /dev/null
+++ b/config/feeds/services.php
@@ -0,0 +1,8 @@
+ [
+ 'key' => env('PODCASTINDEX_ORG_KEY'),
+ 'secret' => env('PODCASTINDEX_ORG_SECRET'),
+ ]
+];
diff --git a/database/factories/EpisodeFactory.php b/database/factories/EpisodeFactory.php
new file mode 100644
index 00000000..3076fd50
--- /dev/null
+++ b/database/factories/EpisodeFactory.php
@@ -0,0 +1,31 @@
+ Podcast::factory(),
+ 'data' => '{}',
+ ];
+ }
+}
diff --git a/database/factories/PodcastFactory.php b/database/factories/PodcastFactory.php
new file mode 100644
index 00000000..90ace2ee
--- /dev/null
+++ b/database/factories/PodcastFactory.php
@@ -0,0 +1,31 @@
+ $this->faker->sentence(4),
+ 'link' => $this->faker->word,
+ 'data' => '{}',
+ ];
+ }
+}
diff --git a/database/migrations/2022_12_04_154911_create_podcasts_table.php b/database/migrations/2022_12_04_154911_create_podcasts_table.php
new file mode 100644
index 00000000..759b87a1
--- /dev/null
+++ b/database/migrations/2022_12_04_154911_create_podcasts_table.php
@@ -0,0 +1,35 @@
+id();
+ $table->string('guid')
+ ->unique();
+ $table->string('title');
+ $table->string('link');
+ $table->string('language_code');
+ $table->json('data');
+ $table->timestamps();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ * @return void
+ */
+ public function down(): void
+ {
+ Schema::dropIfExists('podcasts');
+ }
+}
diff --git a/database/migrations/2022_12_04_154912_create_episodes_table.php b/database/migrations/2022_12_04_154912_create_episodes_table.php
new file mode 100644
index 00000000..981a1b85
--- /dev/null
+++ b/database/migrations/2022_12_04_154912_create_episodes_table.php
@@ -0,0 +1,40 @@
+id();
+ $table->string('guid')
+ ->unique();
+ $table->foreignId('podcast_id')
+ ->constrained()
+ ->cascadeOnDelete()
+ ->cascadeOnUpdate();
+ $table->json('data');
+ $table->timestamps();
+ });
+
+ Schema::enableForeignKeyConstraints();
+ }
+
+ /**
+ * Reverse the migrations.
+ * @return void
+ */
+ public function down(): void
+ {
+ Schema::dropIfExists('episodes');
+ }
+}
diff --git a/database/migrations/2022_12_05_160932_create_libraries_table.php b/database/migrations/2022_12_05_160932_create_libraries_table.php
index e79296ff..ea72aa87 100644
--- a/database/migrations/2022_12_05_160932_create_libraries_table.php
+++ b/database/migrations/2022_12_05_160932_create_libraries_table.php
@@ -14,7 +14,7 @@ class CreateLibrariesTable extends Migration
{
Schema::create('libraries', function (Blueprint $table) {
$table->id();
- $table->string('name');
+ $table->string('name')->unique();
$table->boolean('is_public')
->default(true);
$table->json('language_codes')
diff --git a/database/migrations/2022_12_05_160933_create_library_items_table.php b/database/migrations/2022_12_05_160933_create_library_items_table.php
index d2cd8106..e0c21c72 100644
--- a/database/migrations/2022_12_05_160933_create_library_items_table.php
+++ b/database/migrations/2022_12_05_160933_create_library_items_table.php
@@ -20,6 +20,11 @@ class CreateLibraryItemsTable extends Migration
->constrained()
->cascadeOnDelete()
->cascadeOnUpdate();
+ $table->foreignId('episode_id')
+ ->nullable()
+ ->constrained()
+ ->cascadeOnDelete()
+ ->cascadeOnUpdate();
$table->unsignedInteger('order_column');
$table->string('name');
$table->string('type');
diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php
index e9e9adbe..1788fcda 100644
--- a/database/seeders/DatabaseSeeder.php
+++ b/database/seeders/DatabaseSeeder.php
@@ -4,6 +4,7 @@ namespace Database\Seeders;
// use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use App\Console\Commands\Database\CreateTags;
+use App\Console\Commands\Feed\ReadAndSyncEinundzwanzigPodcastFeed;
use App\Models\Category;
use App\Models\City;
use App\Models\Country;
@@ -277,5 +278,6 @@ class DatabaseSeeder extends Seeder
$libraryItem->syncTagsWithType(['Präsentationen'], 'library_item');
$nonPublicLibrary->libraryItems()
->attach($libraryItem);
+ Artisan::call(ReadAndSyncEinundzwanzigPodcastFeed::class);
}
}
diff --git a/resources/views/columns/library_items/action.blade.php b/resources/views/columns/library_items/action.blade.php
index 3522dd23..430d54d2 100644
--- a/resources/views/columns/library_items/action.blade.php
+++ b/resources/views/columns/library_items/action.blade.php
@@ -11,4 +11,10 @@
Download
@endif
+ @if($row->type === 'podcast_episode')
+