feat: deep link added
This commit is contained in:
@@ -23,7 +23,19 @@ public function index(Request $request)
|
||||
return response()->json($this->playlistCategories($request)->values());
|
||||
}
|
||||
|
||||
$query = Category::query()->withCount('subcategories');
|
||||
// Admin listing of deep-link (hidden) categories: ?deeplink_only=1
|
||||
// returns only the hidden records so the admin can manage their links.
|
||||
if ($request->boolean('deeplink_only')) {
|
||||
$hidden = Category::query()->withCount('subcategories')
|
||||
->where('is_deeplink_only', true)
|
||||
->orderBy('order')
|
||||
->orderBy('name')
|
||||
->get();
|
||||
|
||||
return response()->json($hidden);
|
||||
}
|
||||
|
||||
$query = Category::query()->withCount('subcategories')->where('is_deeplink_only', false);
|
||||
|
||||
if ($request->filled('type')) {
|
||||
$query->where('type', $type);
|
||||
@@ -91,6 +103,7 @@ public function store(Request $request)
|
||||
'order' => 'nullable|integer',
|
||||
'description' => 'nullable|string',
|
||||
'icon' => 'nullable|file|extensions:jpg,jpeg,png,gif,webp,bmp,svg|max:8192',
|
||||
'is_deeplink_only' => 'nullable|boolean',
|
||||
]);
|
||||
|
||||
$category = Category::create([
|
||||
@@ -101,6 +114,7 @@ public function store(Request $request)
|
||||
'icon' => $request->hasFile('icon')
|
||||
? $this->storeUpload($request->file('icon'), 'categories/icons')
|
||||
: null,
|
||||
'is_deeplink_only' => $data['is_deeplink_only'] ?? false,
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
@@ -111,11 +125,45 @@ public function store(Request $request)
|
||||
|
||||
public function show($id)
|
||||
{
|
||||
// Includes hidden (deeplink-only) categories too, so a category reached
|
||||
// through its deep link can still be shown to the app.
|
||||
$category = Category::with('subcategories')->withCount('subcategories')->findOrFail($id);
|
||||
|
||||
return response()->json($category);
|
||||
}
|
||||
|
||||
// Public (no auth) deep link lookup by ID. Hidden (deeplink-only) categories
|
||||
// are the only ones served here, since they are discovered via the deep link.
|
||||
public function publicShow($id)
|
||||
{
|
||||
$category = Category::with(['subcategories' => function ($q) {
|
||||
$q->orderBy('name')->with(['media' => function ($m) {
|
||||
$m->with(['image', 'detailImage', 'tags'])->latest();
|
||||
}]);
|
||||
}])->where('id', $id)->where('is_deeplink_only', true)->firstOrFail();
|
||||
|
||||
return response()->json([
|
||||
'category' => [
|
||||
'id' => $category->id,
|
||||
'name' => $category->name,
|
||||
'description' => $category->description,
|
||||
'icon_url' => $category->icon_url,
|
||||
'icon' => $category->icon,
|
||||
'type' => $category->type,
|
||||
'is_deeplink_only' => $category->is_deeplink_only,
|
||||
'deep_link' => $category->deep_link,
|
||||
],
|
||||
'sub_categories' => $category->subcategories->map(fn ($sub) => [
|
||||
'sub_category' => [
|
||||
'id' => $sub->id,
|
||||
'name' => $sub->name,
|
||||
'description' => $sub->description,
|
||||
],
|
||||
'medias' => $sub->media,
|
||||
])->values(),
|
||||
]);
|
||||
}
|
||||
|
||||
// Category with each of its sub-categories and that sub-category's medias.
|
||||
public function grouped($id)
|
||||
{
|
||||
@@ -163,6 +211,7 @@ public function update(Request $request, $id)
|
||||
'order' => 'nullable|integer',
|
||||
'description' => 'nullable|string',
|
||||
'icon' => 'nullable|file|extensions:jpg,jpeg,png,gif,webp,bmp,svg|max:8192',
|
||||
'is_deeplink_only' => 'nullable|boolean',
|
||||
]);
|
||||
|
||||
if (array_key_exists('type', $data) && $data['type'] !== null) {
|
||||
@@ -184,6 +233,10 @@ public function update(Request $request, $id)
|
||||
$category->icon = $this->storeUpload($request->file('icon'), 'categories/icons');
|
||||
}
|
||||
|
||||
if (array_key_exists('is_deeplink_only', $data)) {
|
||||
$category->is_deeplink_only = $data['is_deeplink_only'];
|
||||
}
|
||||
|
||||
$category->save();
|
||||
|
||||
return response()->json([
|
||||
|
||||
@@ -37,6 +37,7 @@ public function store(Request $request)
|
||||
'file' => 'nullable|file|extensions:mp3,wav,ogg,flac,aac,m4a,opus,mpga,wma,mp4,mov,m4v,webm,mkv,avi,3gp|max:512000',
|
||||
'external_url' => 'nullable|string',
|
||||
'visibility' => 'nullable|in:public,private',
|
||||
'is_deeplink_only' => 'nullable|boolean',
|
||||
|
||||
'tags' => 'nullable|array',
|
||||
'tags.*' => 'string',
|
||||
@@ -66,7 +67,8 @@ public function store(Request $request)
|
||||
'detail_image_id' => $detailImageId,
|
||||
'duration' => $data['duration'] ?? null,
|
||||
'visibility' => $data['visibility'] ?? 'public',
|
||||
'is_premium'=> $data['is_premium'] ?? false
|
||||
'is_premium'=> $data['is_premium'] ?? false,
|
||||
'is_deeplink_only' => $data['is_deeplink_only'] ?? false
|
||||
]);
|
||||
|
||||
$media->categories()->sync($data['category_ids'] ?? []);
|
||||
@@ -89,11 +91,23 @@ public function store(Request $request)
|
||||
}
|
||||
public function index(Request $request)
|
||||
{
|
||||
// Admin listing of deep-link (hidden) media: ?deeplink_only=1 returns only
|
||||
// the hidden records so the admin can manage their share links.
|
||||
if ($request->boolean('deeplink_only')) {
|
||||
$hidden = Media::with(['image', 'detailImage', 'categories', 'subCategories', 'tags'])
|
||||
->where('is_deeplink_only', true)
|
||||
->orderBy('created_at', 'desc')
|
||||
->get();
|
||||
|
||||
return response()->json($hidden);
|
||||
}
|
||||
|
||||
$query = Media::with(['image', 'detailImage', 'categories', 'subCategories', 'myNote', 'tags','comments'])
|
||||
->where(function ($q) {
|
||||
$q->where('visibility', 'public')
|
||||
->orWhere('user_id', auth()->id());
|
||||
});
|
||||
})
|
||||
->where('is_deeplink_only', false);
|
||||
|
||||
$this->applyMediaFilters($query, $request);
|
||||
|
||||
@@ -213,6 +227,7 @@ public function newest(Request $request)
|
||||
$q->where('visibility', 'public')
|
||||
->orWhere('user_id', auth()->id());
|
||||
})
|
||||
->where('is_deeplink_only', false)
|
||||
->orderBy('created_at', 'desc')
|
||||
->paginate($perPage);
|
||||
|
||||
@@ -298,7 +313,8 @@ public function filters(Request $request)
|
||||
->where(function ($q) {
|
||||
$q->where('visibility', 'public')
|
||||
->orWhere('user_id', auth()->id());
|
||||
});
|
||||
})
|
||||
->where('is_deeplink_only', false);
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
@@ -310,7 +326,7 @@ public function filters(Request $request)
|
||||
$q->where(function ($inner) {
|
||||
$inner->where('media.visibility', 'public')
|
||||
->orWhere('media.user_id', auth()->id());
|
||||
});
|
||||
})->where('media.is_deeplink_only', false);
|
||||
};
|
||||
|
||||
$categories = Category::query()
|
||||
@@ -354,6 +370,7 @@ public function filters(Request $request)
|
||||
$q->where('visibility', 'public')
|
||||
->orWhere('user_id', auth()->id());
|
||||
})
|
||||
->where('is_deeplink_only', false)
|
||||
->groupBy('duration_range')
|
||||
->pluck('total', 'duration_range');
|
||||
|
||||
@@ -381,7 +398,8 @@ public function search(Request $request)
|
||||
->where(function ($q) {
|
||||
$q->where('visibility', 'public')
|
||||
->orWhere('user_id', auth()->id());
|
||||
});
|
||||
})
|
||||
->where('is_deeplink_only', false);
|
||||
|
||||
$this->applyMediaFilters($query, $request);
|
||||
|
||||
@@ -412,7 +430,8 @@ public function show($id)
|
||||
->where('id', $id)
|
||||
->where(function($query) {
|
||||
$query->where('visibility', 'public')
|
||||
->orWhere('user_id', auth()->id());
|
||||
->orWhere('user_id', auth()->id())
|
||||
->orWhere('is_deeplink_only', true);
|
||||
})
|
||||
->firstOrFail();
|
||||
|
||||
@@ -471,6 +490,56 @@ public function show($id)
|
||||
]);
|
||||
}
|
||||
|
||||
// Public (no auth) deep link lookup by ID. Returns the same shape as show()
|
||||
// minus the user-specific fields. Hidden (deeplink-only) media is included,
|
||||
// since the deep link is how such media is discovered.
|
||||
public function publicShow($id)
|
||||
{
|
||||
$media = Media::with([
|
||||
'image',
|
||||
'detailImage',
|
||||
'categories',
|
||||
'subCategories',
|
||||
'tags',
|
||||
'comments' => function($query) {
|
||||
$query->with('user')->latest()->limit(10);
|
||||
},
|
||||
])
|
||||
->where('id', $id)
|
||||
->where('is_deeplink_only', true)
|
||||
->firstOrFail();
|
||||
|
||||
return response()->json([
|
||||
'id' => $media->id,
|
||||
'title' => $media->title,
|
||||
'caption' => $media->caption,
|
||||
'type' => $media->type,
|
||||
'file_path' => $media->file_path,
|
||||
'external_url' => $media->external_url,
|
||||
'duration' => $media->duration,
|
||||
'visibility' => $media->visibility,
|
||||
'is_premium' => $media->is_premium,
|
||||
'is_deeplink_only' => $media->is_deeplink_only,
|
||||
'deep_link' => $media->deep_link,
|
||||
'created_at' => $media->created_at,
|
||||
'updated_at' => $media->updated_at,
|
||||
'image' => $media->image,
|
||||
'detail_image' => $media->detailImage,
|
||||
'categories' => $media->categories,
|
||||
'sub_categories' => $media->subCategories,
|
||||
'tags' => $media->tags,
|
||||
'comments' => $media->comments,
|
||||
'statistics' => [
|
||||
'average_rating' => $media->average_rating,
|
||||
'total_ratings' => $media->ratings_count,
|
||||
'total_comments' => $media->comments_count,
|
||||
'total_likes' => $media->likes_count,
|
||||
'total_saves' => $media->saved_count,
|
||||
'rating_distribution' => $media->rating_distribution,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
// Media that shares categories / subcategories / tags with the given one,
|
||||
// ranked by how much they overlap. Excludes the media itself.
|
||||
private function similarMedia(Media $media, int $limit = 5)
|
||||
@@ -488,6 +557,7 @@ private function similarMedia(Media $media, int $limit = 5)
|
||||
return Media::query()
|
||||
->where('media.id', '!=', $media->id)
|
||||
->where('media.type', $media->type)
|
||||
->where('media.is_deeplink_only', false)
|
||||
->where(function ($q) use ($userId) {
|
||||
$q->where('visibility', 'public')->orWhere('user_id', $userId);
|
||||
})
|
||||
@@ -593,6 +663,7 @@ public function update(Request $request, $id)
|
||||
'file' => 'nullable|file|extensions:mp3,wav,ogg,flac,aac,m4a,opus,mpga,wma,mp4,mov,m4v,webm,mkv,avi,3gp|max:512000',
|
||||
'external_url' => 'nullable|string',
|
||||
'visibility' => 'nullable|in:public,private',
|
||||
'is_deeplink_only' => 'nullable|boolean',
|
||||
'tags' => 'nullable|array',
|
||||
'tags.*' => 'string',
|
||||
]);
|
||||
@@ -616,6 +687,7 @@ public function update(Request $request, $id)
|
||||
'duration' => $data['duration'] ?? $media->duration,
|
||||
'external_url' => $data['external_url'] ?? $media->external_url,
|
||||
'visibility' => $data['visibility'] ?? $media->visibility,
|
||||
'is_deeplink_only' => array_key_exists('is_deeplink_only', $data) ? $data['is_deeplink_only'] : $media->is_deeplink_only,
|
||||
'file_path' => $data['file_path'] ?? $media->file_path,
|
||||
];
|
||||
|
||||
@@ -741,6 +813,7 @@ public function popular(Request $request)
|
||||
->where(function ($q) {
|
||||
$q->where('visibility', 'public')->orWhere('user_id', auth()->id());
|
||||
})
|
||||
->where('is_deeplink_only', false)
|
||||
->withCount('plays as listeners_count') // distinct users who played
|
||||
->withSum('plays as plays_count', 'play_count') // total plays
|
||||
->with(['image', 'detailImage', 'categories', 'subCategories', 'tags'])
|
||||
@@ -760,7 +833,7 @@ public function recentlyPlayed(Request $request)
|
||||
|
||||
$plays = MediaPlay::where('user_id', auth()->id())
|
||||
->whereNotNull('last_played_at')
|
||||
->with(['media' => fn ($q) => $q->with(['image', 'detailImage', 'categories', 'subCategories', 'tags'])])
|
||||
->with(['media' => fn ($q) => $q->where('is_deeplink_only', false)->with(['image', 'detailImage', 'categories', 'subCategories', 'tags'])])
|
||||
->orderByDesc('last_played_at')
|
||||
->limit($limit)
|
||||
->get();
|
||||
|
||||
+14
-2
@@ -16,19 +16,31 @@ class Category extends Model
|
||||
self::TYPE_BREATHING_TEMPLATE,
|
||||
];
|
||||
|
||||
protected $fillable = ['name', 'type', 'order', 'description', 'icon'];
|
||||
protected $fillable = ['name', 'type', 'order', 'description', 'icon', 'is_deeplink_only'];
|
||||
|
||||
protected $casts = [
|
||||
'order' => 'integer',
|
||||
'is_deeplink_only' => 'boolean',
|
||||
];
|
||||
|
||||
protected $appends = ['icon_url'];
|
||||
protected $appends = ['icon_url', 'deep_link'];
|
||||
|
||||
public function getIconUrlAttribute(): ?string
|
||||
{
|
||||
return $this->icon ? asset('storage/' . $this->icon) : null;
|
||||
}
|
||||
|
||||
public function getDeepLinkAttribute(): ?string
|
||||
{
|
||||
$canonical = config('deeplinks.category_pattern');
|
||||
|
||||
if ($canonical) {
|
||||
return rtrim(str_replace('{id}', $this->id, $canonical), '/');
|
||||
}
|
||||
|
||||
return url('/category/' . $this->id);
|
||||
}
|
||||
|
||||
public function questions()
|
||||
{
|
||||
return $this->hasMany(Question::class);
|
||||
|
||||
+19
-1
@@ -21,7 +21,12 @@ class Media extends Model
|
||||
'external_url',
|
||||
'duration',
|
||||
'visibility',
|
||||
'is_premium'
|
||||
'is_premium',
|
||||
'is_deeplink_only'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'is_deeplink_only' => 'boolean',
|
||||
];
|
||||
protected $appends = [
|
||||
'average_rating',
|
||||
@@ -37,6 +42,7 @@ class Media extends Model
|
||||
'is_liked',
|
||||
'likes_count',
|
||||
'url',
|
||||
'deep_link',
|
||||
];
|
||||
public function image()
|
||||
{
|
||||
@@ -89,6 +95,18 @@ public function getUrlAttribute()
|
||||
|
||||
return $this->external_url;
|
||||
}
|
||||
|
||||
// Public deep link for this media (canonical aramejan.com form).
|
||||
public function getDeepLinkAttribute(): ?string
|
||||
{
|
||||
$canonical = config('deeplinks.media_pattern');
|
||||
|
||||
if ($canonical) {
|
||||
return rtrim(str_replace('{id}', $this->id, $canonical), '/');
|
||||
}
|
||||
|
||||
return url('/media/' . $this->id);
|
||||
}
|
||||
// is_saved / saved_count come from the HasSaves trait (saved_items table),
|
||||
// so the /saves/* endpoints and the media list/show all agree.
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user