Compare commits
3
Commits
3f3b5b1d69
...
ebe5f50145
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ebe5f50145 | ||
|
|
78ea663c2b | ||
|
|
a3cda12560 |
@@ -23,7 +23,19 @@ public function index(Request $request)
|
|||||||
return response()->json($this->playlistCategories($request)->values());
|
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')) {
|
if ($request->filled('type')) {
|
||||||
$query->where('type', $type);
|
$query->where('type', $type);
|
||||||
@@ -91,6 +103,7 @@ public function store(Request $request)
|
|||||||
'order' => 'nullable|integer',
|
'order' => 'nullable|integer',
|
||||||
'description' => 'nullable|string',
|
'description' => 'nullable|string',
|
||||||
'icon' => 'nullable|file|extensions:jpg,jpeg,png,gif,webp,bmp,svg|max:8192',
|
'icon' => 'nullable|file|extensions:jpg,jpeg,png,gif,webp,bmp,svg|max:8192',
|
||||||
|
'is_deeplink_only' => 'nullable|boolean',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$category = Category::create([
|
$category = Category::create([
|
||||||
@@ -101,6 +114,7 @@ public function store(Request $request)
|
|||||||
'icon' => $request->hasFile('icon')
|
'icon' => $request->hasFile('icon')
|
||||||
? $this->storeUpload($request->file('icon'), 'categories/icons')
|
? $this->storeUpload($request->file('icon'), 'categories/icons')
|
||||||
: null,
|
: null,
|
||||||
|
'is_deeplink_only' => $data['is_deeplink_only'] ?? false,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
@@ -111,17 +125,62 @@ public function store(Request $request)
|
|||||||
|
|
||||||
public function show($id)
|
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);
|
$category = Category::with('subcategories')->withCount('subcategories')->findOrFail($id);
|
||||||
|
|
||||||
return response()->json($category);
|
return response()->json($category);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Public (no auth) deep link lookup by ID. Serves any category — hidden
|
||||||
|
// (deeplink-only) ones included — so https://aramejan.com/category/{id}
|
||||||
|
// resolves for every shareable category. A visible category only lists its
|
||||||
|
// visible media; inside a hidden category everything is shown, since the
|
||||||
|
// deep link is the only way in.
|
||||||
|
public function publicShow($id)
|
||||||
|
{
|
||||||
|
$category = Category::findOrFail($id);
|
||||||
|
$includeHiddenMedia = (bool) $category->is_deeplink_only;
|
||||||
|
|
||||||
|
$category->load(['subcategories' => function ($q) use ($includeHiddenMedia) {
|
||||||
|
$q->orderBy('name')->with(['media' => function ($m) use ($includeHiddenMedia) {
|
||||||
|
if (! $includeHiddenMedia) {
|
||||||
|
$m->where('is_deeplink_only', false);
|
||||||
|
}
|
||||||
|
$m->with(['image', 'detailImage', 'tags'])->latest();
|
||||||
|
}]);
|
||||||
|
}]);
|
||||||
|
|
||||||
|
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.
|
// Category with each of its sub-categories and that sub-category's medias.
|
||||||
public function grouped($id)
|
public function grouped($id)
|
||||||
{
|
{
|
||||||
$category = Category::with(['subcategories' => function ($q) {
|
$category = Category::with(['subcategories' => function ($q) {
|
||||||
$q->orderBy('name')->with(['media' => function ($m) {
|
$q->orderBy('name')->with(['media' => function ($m) {
|
||||||
$m->with(['image', 'detailImage', 'tags'])->latest();
|
// Deep-link-only media stays out of the normal category screen.
|
||||||
|
$m->where('is_deeplink_only', false)
|
||||||
|
->with(['image', 'detailImage', 'tags'])->latest();
|
||||||
}]);
|
}]);
|
||||||
}])->findOrFail($id);
|
}])->findOrFail($id);
|
||||||
|
|
||||||
@@ -163,6 +222,7 @@ public function update(Request $request, $id)
|
|||||||
'order' => 'nullable|integer',
|
'order' => 'nullable|integer',
|
||||||
'description' => 'nullable|string',
|
'description' => 'nullable|string',
|
||||||
'icon' => 'nullable|file|extensions:jpg,jpeg,png,gif,webp,bmp,svg|max:8192',
|
'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) {
|
if (array_key_exists('type', $data) && $data['type'] !== null) {
|
||||||
@@ -184,6 +244,10 @@ public function update(Request $request, $id)
|
|||||||
$category->icon = $this->storeUpload($request->file('icon'), 'categories/icons');
|
$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();
|
$category->save();
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
|
|||||||
@@ -17,6 +17,20 @@ class MediaController extends Controller
|
|||||||
{
|
{
|
||||||
use HandlesImageUpload, StoresUploads;
|
use HandlesImageUpload, StoresUploads;
|
||||||
|
|
||||||
|
// Media the caller may modify: their own, plus everything for an admin
|
||||||
|
// token (same `admin` ability the admin-only routes are gated on). Without
|
||||||
|
// this the panel gets a 404 when editing media uploaded by someone else.
|
||||||
|
private function editableMedia($id): Media
|
||||||
|
{
|
||||||
|
$query = Media::where('id', $id);
|
||||||
|
|
||||||
|
if (! auth()->user()?->tokenCan('admin')) {
|
||||||
|
$query->where('user_id', auth()->id());
|
||||||
|
}
|
||||||
|
|
||||||
|
return $query->firstOrFail();
|
||||||
|
}
|
||||||
|
|
||||||
// CREATE media
|
// CREATE media
|
||||||
public function store(Request $request)
|
public function store(Request $request)
|
||||||
{
|
{
|
||||||
@@ -37,6 +51,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',
|
'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',
|
'external_url' => 'nullable|string',
|
||||||
'visibility' => 'nullable|in:public,private',
|
'visibility' => 'nullable|in:public,private',
|
||||||
|
'is_deeplink_only' => 'nullable|boolean',
|
||||||
|
|
||||||
'tags' => 'nullable|array',
|
'tags' => 'nullable|array',
|
||||||
'tags.*' => 'string',
|
'tags.*' => 'string',
|
||||||
@@ -66,7 +81,8 @@ public function store(Request $request)
|
|||||||
'detail_image_id' => $detailImageId,
|
'detail_image_id' => $detailImageId,
|
||||||
'duration' => $data['duration'] ?? null,
|
'duration' => $data['duration'] ?? null,
|
||||||
'visibility' => $data['visibility'] ?? 'public',
|
'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'] ?? []);
|
$media->categories()->sync($data['category_ids'] ?? []);
|
||||||
@@ -89,11 +105,28 @@ public function store(Request $request)
|
|||||||
}
|
}
|
||||||
public function index(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();
|
||||||
|
|
||||||
|
// Same external_url fallback as the normal listing below.
|
||||||
|
$hidden->each(function ($m) {
|
||||||
|
$m->external_url = $m->external_url ?: ($m->url ?? '');
|
||||||
|
});
|
||||||
|
|
||||||
|
return response()->json($hidden);
|
||||||
|
}
|
||||||
|
|
||||||
$query = Media::with(['image', 'detailImage', 'categories', 'subCategories', 'myNote', 'tags','comments'])
|
$query = Media::with(['image', 'detailImage', 'categories', 'subCategories', 'myNote', 'tags','comments'])
|
||||||
->where(function ($q) {
|
->where(function ($q) {
|
||||||
$q->where('visibility', 'public')
|
$q->where('visibility', 'public')
|
||||||
->orWhere('user_id', auth()->id());
|
->orWhere('user_id', auth()->id());
|
||||||
});
|
})
|
||||||
|
->where('is_deeplink_only', false);
|
||||||
|
|
||||||
$this->applyMediaFilters($query, $request);
|
$this->applyMediaFilters($query, $request);
|
||||||
|
|
||||||
@@ -213,6 +246,7 @@ public function newest(Request $request)
|
|||||||
$q->where('visibility', 'public')
|
$q->where('visibility', 'public')
|
||||||
->orWhere('user_id', auth()->id());
|
->orWhere('user_id', auth()->id());
|
||||||
})
|
})
|
||||||
|
->where('is_deeplink_only', false)
|
||||||
->orderBy('created_at', 'desc')
|
->orderBy('created_at', 'desc')
|
||||||
->paginate($perPage);
|
->paginate($perPage);
|
||||||
|
|
||||||
@@ -298,7 +332,8 @@ public function filters(Request $request)
|
|||||||
->where(function ($q) {
|
->where(function ($q) {
|
||||||
$q->where('visibility', 'public')
|
$q->where('visibility', 'public')
|
||||||
->orWhere('user_id', auth()->id());
|
->orWhere('user_id', auth()->id());
|
||||||
});
|
})
|
||||||
|
->where('is_deeplink_only', false);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
@@ -310,7 +345,7 @@ public function filters(Request $request)
|
|||||||
$q->where(function ($inner) {
|
$q->where(function ($inner) {
|
||||||
$inner->where('media.visibility', 'public')
|
$inner->where('media.visibility', 'public')
|
||||||
->orWhere('media.user_id', auth()->id());
|
->orWhere('media.user_id', auth()->id());
|
||||||
});
|
})->where('media.is_deeplink_only', false);
|
||||||
};
|
};
|
||||||
|
|
||||||
$categories = Category::query()
|
$categories = Category::query()
|
||||||
@@ -354,6 +389,7 @@ public function filters(Request $request)
|
|||||||
$q->where('visibility', 'public')
|
$q->where('visibility', 'public')
|
||||||
->orWhere('user_id', auth()->id());
|
->orWhere('user_id', auth()->id());
|
||||||
})
|
})
|
||||||
|
->where('is_deeplink_only', false)
|
||||||
->groupBy('duration_range')
|
->groupBy('duration_range')
|
||||||
->pluck('total', 'duration_range');
|
->pluck('total', 'duration_range');
|
||||||
|
|
||||||
@@ -381,7 +417,8 @@ public function search(Request $request)
|
|||||||
->where(function ($q) {
|
->where(function ($q) {
|
||||||
$q->where('visibility', 'public')
|
$q->where('visibility', 'public')
|
||||||
->orWhere('user_id', auth()->id());
|
->orWhere('user_id', auth()->id());
|
||||||
});
|
})
|
||||||
|
->where('is_deeplink_only', false);
|
||||||
|
|
||||||
$this->applyMediaFilters($query, $request);
|
$this->applyMediaFilters($query, $request);
|
||||||
|
|
||||||
@@ -412,7 +449,8 @@ public function show($id)
|
|||||||
->where('id', $id)
|
->where('id', $id)
|
||||||
->where(function($query) {
|
->where(function($query) {
|
||||||
$query->where('visibility', 'public')
|
$query->where('visibility', 'public')
|
||||||
->orWhere('user_id', auth()->id());
|
->orWhere('user_id', auth()->id())
|
||||||
|
->orWhere('is_deeplink_only', true);
|
||||||
})
|
})
|
||||||
->firstOrFail();
|
->firstOrFail();
|
||||||
|
|
||||||
@@ -471,6 +509,60 @@ public function show($id)
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Public (no auth) deep link lookup by ID. Returns the same shape as show()
|
||||||
|
// minus the user-specific fields. Serves any publicly visible media plus the
|
||||||
|
// hidden (deeplink-only) ones, since the deep link is how those are discovered
|
||||||
|
// — https://aramejan.com/media/{id} must resolve for every shareable media.
|
||||||
|
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(function ($q) {
|
||||||
|
$q->where('visibility', 'public')
|
||||||
|
->orWhere('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,
|
// Media that shares categories / subcategories / tags with the given one,
|
||||||
// ranked by how much they overlap. Excludes the media itself.
|
// ranked by how much they overlap. Excludes the media itself.
|
||||||
private function similarMedia(Media $media, int $limit = 5)
|
private function similarMedia(Media $media, int $limit = 5)
|
||||||
@@ -488,6 +580,7 @@ private function similarMedia(Media $media, int $limit = 5)
|
|||||||
return Media::query()
|
return Media::query()
|
||||||
->where('media.id', '!=', $media->id)
|
->where('media.id', '!=', $media->id)
|
||||||
->where('media.type', $media->type)
|
->where('media.type', $media->type)
|
||||||
|
->where('media.is_deeplink_only', false)
|
||||||
->where(function ($q) use ($userId) {
|
->where(function ($q) use ($userId) {
|
||||||
$q->where('visibility', 'public')->orWhere('user_id', $userId);
|
$q->where('visibility', 'public')->orWhere('user_id', $userId);
|
||||||
})
|
})
|
||||||
@@ -571,9 +664,7 @@ public function getComments($mediaId)
|
|||||||
// UPDATE media
|
// UPDATE media
|
||||||
public function update(Request $request, $id)
|
public function update(Request $request, $id)
|
||||||
{
|
{
|
||||||
$media = Media::where('id', $id)
|
$media = $this->editableMedia($id);
|
||||||
->where('user_id', auth()->id())
|
|
||||||
->firstOrFail();
|
|
||||||
|
|
||||||
$data = $request->validate([
|
$data = $request->validate([
|
||||||
'title' => 'nullable|string',
|
'title' => 'nullable|string',
|
||||||
@@ -593,6 +684,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',
|
'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',
|
'external_url' => 'nullable|string',
|
||||||
'visibility' => 'nullable|in:public,private',
|
'visibility' => 'nullable|in:public,private',
|
||||||
|
'is_deeplink_only' => 'nullable|boolean',
|
||||||
'tags' => 'nullable|array',
|
'tags' => 'nullable|array',
|
||||||
'tags.*' => 'string',
|
'tags.*' => 'string',
|
||||||
]);
|
]);
|
||||||
@@ -616,6 +708,7 @@ public function update(Request $request, $id)
|
|||||||
'duration' => $data['duration'] ?? $media->duration,
|
'duration' => $data['duration'] ?? $media->duration,
|
||||||
'external_url' => $data['external_url'] ?? $media->external_url,
|
'external_url' => $data['external_url'] ?? $media->external_url,
|
||||||
'visibility' => $data['visibility'] ?? $media->visibility,
|
'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,
|
'file_path' => $data['file_path'] ?? $media->file_path,
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -672,9 +765,7 @@ public function update(Request $request, $id)
|
|||||||
|
|
||||||
public function destroy($id)
|
public function destroy($id)
|
||||||
{
|
{
|
||||||
$media = Media::where('id', $id)
|
$media = $this->editableMedia($id);
|
||||||
->where('user_id', auth()->id())
|
|
||||||
->firstOrFail();
|
|
||||||
|
|
||||||
if ($media->file_path) {
|
if ($media->file_path) {
|
||||||
Storage::disk('public')->delete($media->file_path);
|
Storage::disk('public')->delete($media->file_path);
|
||||||
@@ -741,6 +832,7 @@ public function popular(Request $request)
|
|||||||
->where(function ($q) {
|
->where(function ($q) {
|
||||||
$q->where('visibility', 'public')->orWhere('user_id', auth()->id());
|
$q->where('visibility', 'public')->orWhere('user_id', auth()->id());
|
||||||
})
|
})
|
||||||
|
->where('is_deeplink_only', false)
|
||||||
->withCount('plays as listeners_count') // distinct users who played
|
->withCount('plays as listeners_count') // distinct users who played
|
||||||
->withSum('plays as plays_count', 'play_count') // total plays
|
->withSum('plays as plays_count', 'play_count') // total plays
|
||||||
->with(['image', 'detailImage', 'categories', 'subCategories', 'tags'])
|
->with(['image', 'detailImage', 'categories', 'subCategories', 'tags'])
|
||||||
@@ -760,7 +852,7 @@ public function recentlyPlayed(Request $request)
|
|||||||
|
|
||||||
$plays = MediaPlay::where('user_id', auth()->id())
|
$plays = MediaPlay::where('user_id', auth()->id())
|
||||||
->whereNotNull('last_played_at')
|
->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')
|
->orderByDesc('last_played_at')
|
||||||
->limit($limit)
|
->limit($limit)
|
||||||
->get();
|
->get();
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ class SubCategoryController extends Controller
|
|||||||
{
|
{
|
||||||
public function index(Request $request, $categoryId = null)
|
public function index(Request $request, $categoryId = null)
|
||||||
{
|
{
|
||||||
$query = SubCategory::with('category')->withCount('media');
|
$query = SubCategory::with('category')->withCount(['media' => fn ($q) => $q->where('is_deeplink_only', false)]);
|
||||||
|
|
||||||
$categoryId = $categoryId ?? $request->input('category_id');
|
$categoryId = $categoryId ?? $request->input('category_id');
|
||||||
|
|
||||||
@@ -51,7 +51,7 @@ public function store(Request $request)
|
|||||||
|
|
||||||
public function show($id)
|
public function show($id)
|
||||||
{
|
{
|
||||||
$subCategory = SubCategory::with('category')->withCount('media')->findOrFail($id);
|
$subCategory = SubCategory::with('category')->withCount(['media' => fn ($q) => $q->where('is_deeplink_only', false)])->findOrFail($id);
|
||||||
|
|
||||||
return response()->json($subCategory);
|
return response()->json($subCategory);
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-2
@@ -16,19 +16,31 @@ class Category extends Model
|
|||||||
self::TYPE_BREATHING_TEMPLATE,
|
self::TYPE_BREATHING_TEMPLATE,
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $fillable = ['name', 'type', 'order', 'description', 'icon'];
|
protected $fillable = ['name', 'type', 'order', 'description', 'icon', 'is_deeplink_only'];
|
||||||
|
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
'order' => 'integer',
|
'order' => 'integer',
|
||||||
|
'is_deeplink_only' => 'boolean',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $appends = ['icon_url'];
|
protected $appends = ['icon_url', 'deep_link'];
|
||||||
|
|
||||||
public function getIconUrlAttribute(): ?string
|
public function getIconUrlAttribute(): ?string
|
||||||
{
|
{
|
||||||
return $this->icon ? asset('storage/' . $this->icon) : null;
|
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()
|
public function questions()
|
||||||
{
|
{
|
||||||
return $this->hasMany(Question::class);
|
return $this->hasMany(Question::class);
|
||||||
|
|||||||
+19
-1
@@ -21,7 +21,12 @@ class Media extends Model
|
|||||||
'external_url',
|
'external_url',
|
||||||
'duration',
|
'duration',
|
||||||
'visibility',
|
'visibility',
|
||||||
'is_premium'
|
'is_premium',
|
||||||
|
'is_deeplink_only'
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'is_deeplink_only' => 'boolean',
|
||||||
];
|
];
|
||||||
protected $appends = [
|
protected $appends = [
|
||||||
'average_rating',
|
'average_rating',
|
||||||
@@ -37,6 +42,7 @@ class Media extends Model
|
|||||||
'is_liked',
|
'is_liked',
|
||||||
'likes_count',
|
'likes_count',
|
||||||
'url',
|
'url',
|
||||||
|
'deep_link',
|
||||||
];
|
];
|
||||||
public function image()
|
public function image()
|
||||||
{
|
{
|
||||||
@@ -89,6 +95,18 @@ public function getUrlAttribute()
|
|||||||
|
|
||||||
return $this->external_url;
|
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),
|
// is_saved / saved_count come from the HasSaves trait (saved_items table),
|
||||||
// so the /saves/* endpoints and the media list/show all agree.
|
// so the /saves/* endpoints and the media list/show all agree.
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
// Deep link / share URL configuration. The canonical domain the mobile app and
|
||||||
|
// web share deep links in (e.g. https://aramejan.com), with {id} placeholders
|
||||||
|
// (the ID of the media / category).
|
||||||
|
$base = rtrim(env('DEEPLINK_BASE_URL', 'https://aramejan.com'), '/');
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
// Canonical deep link domain (without trailing slash).
|
||||||
|
'base_url' => $base,
|
||||||
|
|
||||||
|
// Pattern used for media deep links.
|
||||||
|
'media_pattern' => env('DEEPLINK_MEDIA_PATTERN', $base . '/media/{id}'),
|
||||||
|
|
||||||
|
// Pattern used for category deep links.
|
||||||
|
'category_pattern' => env('DEEPLINK_CATEGORY_PATTERN', $base . '/category/{id}'),
|
||||||
|
];
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('media', function (Blueprint $table) {
|
||||||
|
// When true the media is hidden from all lists/search but still
|
||||||
|
// reachable through its deep link (https://aramejan.com/media/{id}).
|
||||||
|
$table->boolean('is_deeplink_only')->default(false)->after('is_premium');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('media', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('is_deeplink_only');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('categories', function (Blueprint $table) {
|
||||||
|
// When true the category is hidden from list/search but still
|
||||||
|
// reachable through its deep link (https://aramejan.com/category/{id}).
|
||||||
|
$table->boolean('is_deeplink_only')->default(false)->after('icon');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('categories', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('is_deeplink_only');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -52,6 +52,12 @@
|
|||||||
|
|
||||||
|
|
||||||
Route::get('package-names/{name}/products', [ProductController::class, 'index']);
|
Route::get('package-names/{name}/products', [ProductController::class, 'index']);
|
||||||
|
|
||||||
|
// Public deep links — allow hidden (is_deeplink_only) media/categories to be
|
||||||
|
// fetched without authentication when reached via their share link.
|
||||||
|
Route::get('public/media/{id}', [MediaController::class, 'publicShow']);
|
||||||
|
Route::get('public/categories/{id}', [CategoryController::class, 'publicShow']);
|
||||||
|
|
||||||
Route::prefix('auth')->controller(UserController::class)->group(function () {
|
Route::prefix('auth')->controller(UserController::class)->group(function () {
|
||||||
Route::post('login-with-approo-v2', [UserController::class, 'loginV2']);
|
Route::post('login-with-approo-v2', [UserController::class, 'loginV2']);
|
||||||
Route::middleware(['throttle:3,1'])->post('login-otp', 'loginOTP');
|
Route::middleware(['throttle:3,1'])->post('login-otp', 'loginOTP');
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use App\Http\Controllers\CategoryController;
|
||||||
|
use App\Http\Controllers\MediaController;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
Route::get('/', function () {
|
Route::get('/', function () {
|
||||||
return view('welcome');
|
return view('welcome');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Canonical deep links (e.g. https://aramejan.com/media/3). Served as JSON so
|
||||||
|
// the mobile app / web can fetch the hidden item reached through the deep link.
|
||||||
|
Route::get('/media/{id}', [MediaController::class, 'publicShow']);
|
||||||
|
Route::get('/category/{id}', [CategoryController::class, 'publicShow']);
|
||||||
Reference in New Issue
Block a user