Skip to content

Commit

Permalink
Merge pull request #31 from karlomikus/develop
Browse files Browse the repository at this point in the history
Merge v102
  • Loading branch information
karlomikus committed Dec 11, 2022
2 parents 778079b + e6c2256 commit d7c253a
Show file tree
Hide file tree
Showing 23 changed files with 802 additions and 505 deletions.
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,18 @@
# v1.0.2
## New
- Add `php artisan bar:scrap` command to scrape recipes from the supported websites
- Support for TuxedoNo2
- Support for A Couple Cooks
- Add cocktails thumbnail generation endpoint
- Enabled GD extension in docker

## Fixes
- Sort ingredient categories by name
- Sort related cocktails in ingredient resource by name

## Changes
- Use `docker-php-extension-installer` for docker image

# v1.0.1
## New
- Make cocktail `id` attribute filterable in cocktails index
Expand Down
6 changes: 3 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ RUN apt update \
&& apt-get autoremove -y \
&& apt-get clean

RUN docker-php-ext-install opcache \
&& pecl install redis \
&& docker-php-ext-enable redis
ADD https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions /usr/local/bin/
RUN chmod +x /usr/local/bin/install-php-extensions && \
install-php-extensions gd opcache redis

# Setup default apache stuff
RUN echo "ServerName localhost" >> /etc/apache2/apache2.conf
Expand Down
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ This application is made with Laravel, so you should check out [deployment requi
The basic requirements are:

- PHP >= 8.1
- GD Extension
- Sqlite 3
- Working [Meilisearch server](https://github.com/meilisearch) instance (v0.29)
- (Optional) Redis server instance
Expand Down Expand Up @@ -79,6 +80,33 @@ Docker image exposes the `/var/www/cocktails/storage` volume, and there is curre

Bar Assistant is using Meilisearch as a primary [Scout driver](https://laravel.com/docs/9.x/scout). It's main purpose is to index cocktails and ingredients and power filtering and searching on the frontend. Checkout [this guide here](https://docs.meilisearch.com/learn/cookbooks/docker.html) on how to setup Meilisearch docker instance.

### Database file backup

You can copy the whole .sqlite file database with the following:

``` bash
# Via docker
$ docker cp bar-assistant:/var/www/cocktails/storage/database.sqlite /path/on/host

# Via docker compose
$ docker compose cp bar-assistant:/var/www/cocktails/storage/database.sqlite /path/on/host
```

### Database dump SQL

You can dump your database to .sql file using the following:

``` bash
# Via cli
$ sqlite3 /var/www/cocktails/storage/database.sqlite .dump > mydump.sql

# Via docker
$ docker exec bar-assistant sqlite3 /var/www/cocktails/storage/database.sqlite .dump > mydump.sql

# Via docker compose
$ docker compose exec bar-assistant sqlite3 /var/www/cocktails/storage/database.sqlite .dump > mydump.sql
```

## Manual setup

After cloning the repository, you should do the following:
Expand Down Expand Up @@ -117,6 +145,9 @@ $ php artisan migrate --force

# To fill the database with data
$ php artisan bar:open

# Or with specific email and password
$ php artisan bar:open [email protected] --pass=12345
```

## Usage
Expand Down
79 changes: 11 additions & 68 deletions app/Console/Commands/BarScrape.php
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
<?php

declare(strict_types=1);

namespace Kami\Cocktail\Console\Commands;

use Illuminate\Support\Str;
use Illuminate\Console\Command;
use Kami\Cocktail\Models\Glass;
use Kami\Cocktail\Models\Image;
use Illuminate\Support\Facades\DB;
use Kami\Cocktail\Scraper\Manager;
use Kami\Cocktail\Services\CocktailService;
use Kami\Cocktail\Services\IngredientService;
use Intervention\Image\ImageManagerStatic as InterventionImage;
use Kami\Cocktail\Services\ImportService;

class BarScrape extends Command
{
Expand All @@ -19,88 +15,35 @@ class BarScrape extends Command
*
* @var string
*/
protected $signature = 'bar:scrape {url}';
protected $signature = 'bar:scrape {url : URL of the recipe} {--i|skip-ingredients : Do not add ingredients} {--tags= : Overwrite tags, seperated by comma}';

/**
* The console command description.
*
* @var string
*/
protected $description = 'Command description';
protected $description = 'Import cocktail recipe from a given URL';

/**
* Execute the console command.
*
* @return int
*/
public function handle()
public function handle(): int
{
$scraper = Manager::scrape($this->argument('url'));

$scrapedData = $scraper->toArray();

/** @var IngredientService */
$ingredientService = app(IngredientService::class);
/** @var CocktailService */
$cocktailService = app(CocktailService::class);

$dbIngredients = DB::table('ingredients')->select('id', DB::raw('LOWER(name) AS name'))->get()->keyBy('name');
$dbGlasses = DB::table('glasses')->select('id', DB::raw('LOWER(name) AS name'))->get()->keyBy('name');

$cocktailImages = [];
if ($scrapedData['image']['url']) {
$memImage = InterventionImage::make($scrapedData['image']['url']);

$filepath = 'temp/' . Str::random(40) . '.jpg';
$memImage->save(storage_path('uploads/' . $filepath));

$image = new Image();
$image->copyright = $scrapedData['image']['copyright'] ?? null;
$image->file_path = $filepath;
$image->file_extension = 'jpg';
$image->save();

$cocktailImages[] = $image->id;
}

// Match ingredients
foreach ($scrapedData['ingredients'] as &$scrapedIngredient) {
if ($dbIngredients->has(strtolower($scrapedIngredient['name']))) {
$scrapedIngredient['ingredient_id'] = $dbIngredients->get(strtolower($scrapedIngredient['name']))->id;
} else {
$this->info('Creating a new ingredient: ' . $scrapedIngredient['name']);
$newIngredient = $ingredientService->createIngredient(ucfirst($scrapedIngredient['name']), 1, 1, description: 'Created by scraper from ' . $scrapedData['source']);
$dbIngredients->put(strtolower($scrapedIngredient['name']), $newIngredient->id);
$scrapedIngredient['ingredient_id'] = $newIngredient->id;
}
if ($this->option('skip-ingredients')) {
$scrapedData['ingredients'] = [];
}

// Match glass
$glassId = null;
if ($dbGlasses->has(strtolower($scrapedData['glass']))) {
$glassId = $dbGlasses->get(strtolower($scrapedData['glass']))->id;
} elseif ($scrapedData['glass'] !== null) {
$this->info('Creating a new glass type: ' . $scrapedData['glass']);
$newGlass = new Glass();
$newGlass->name = ucfirst($scrapedData['glass']);
$newGlass->description = 'Created by scraper from ' . $scrapedData['source'];
$newGlass->save();
$dbGlasses->put(strtolower($scrapedData['glass']), $newGlass->id);
$glassId = $newGlass->id;
if ($this->option('tags')) {
$scrapedData['tags'] = explode(',', $this->option('tags'));
}

$cocktailService->createCocktail(
$scrapedData['name'],
$scrapedData['instructions'],
$scrapedData['ingredients'],
1,
$scrapedData['description'],
$scrapedData['garnish'],
$scrapedData['source'],
$cocktailImages,
$scrapedData['tags'],
$glassId
);
resolve(ImportService::class)->import($scrapedData);

return Command::SUCCESS;
}
Expand Down
25 changes: 25 additions & 0 deletions app/Http/Controllers/ImageController.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Kami\Cocktail\Models\Image;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Storage;
use Kami\Cocktail\Services\ImageService;
use Intervention\Image\ImageManagerStatic;
use Kami\Cocktail\Http\Requests\ImageRequest;
use Kami\Cocktail\Http\Resources\ImageResource;
use Illuminate\Http\Resources\Json\JsonResource;
Expand Down Expand Up @@ -41,4 +44,26 @@ public function delete(int $id): Response

return response(null, 204);
}

public function thumb(int $id)
{
[$content, $etag] = Cache::remember('image_thumb_' . $id, 1 * 24 * 60 * 60, function () use ($id) {
$dbImage = Image::findOrFail($id);
$disk = Storage::disk('app_images');
$responseContent = (string) ImageManagerStatic::make($disk->get($dbImage->file_path))->fit(200, 200)->encode();
$etag = md5($dbImage->id . '-' . $dbImage->updated_at->format('Y-m-d H:i:s'));

return [$responseContent, $etag];
});

$mime = finfo_buffer(finfo_open(FILEINFO_MIME_TYPE), $content);
$notModified = isset($_SERVER['HTTP_IF_NONE_MATCH']) && $_SERVER['HTTP_IF_NONE_MATCH'] == $etag;
$statusCode = $notModified ? 304 : 200;

return new Response($content, $statusCode, [
'Content-Type' => $mime,
'Content-Length' => strlen($content),
'Etag' => $etag
]);
}
}
2 changes: 1 addition & 1 deletion app/Http/Controllers/IngredientCategoryController.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class IngredientCategoryController extends Controller
{
public function index(): JsonResource
{
$categories = IngredientCategory::all();
$categories = IngredientCategory::orderBy('name')->get();

return IngredientCategoryResource::collection($categories);
}
Expand Down
4 changes: 2 additions & 2 deletions app/Http/Resources/IngredientResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public function toArray($request)
'slug' => $this->slug,
'name' => $this->name,
'strength' => $this->strength,
'description' => $this->description,
'description' => e($this->description),
'origin' => $this->origin,
'main_image_id' => $this->images->first()->id ?? null,
'images' => ImageResource::collection($this->images),
Expand All @@ -48,7 +48,7 @@ public function toArray($request)
'slug' => $c->slug,
'name' => $c->name,
];
});
})->sortBy('name')->toArray();
})
];
}
Expand Down
1 change: 1 addition & 0 deletions app/Models/Cocktail.php
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ public function toSearchableArray(): array
'source' => $this->source,
'garnish' => $this->garnish,
'image_url' => $this->getMainImageUrl(),
'main_image_id' => $this->images->first()?->id ?? null,
'short_ingredients' => $this->ingredients->pluck('ingredient.name'),
'user_id' => $this->user_id,
'tags' => $this->tags->pluck('name'),
Expand Down
15 changes: 15 additions & 0 deletions app/Scraper/HasJsonLd.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

namespace Kami\Cocktail\Scraper;

trait HasJsonLd
{
public function parseSchema(): ?array
{
$jsonLdSchema = $this->crawler->filterXPath('//script[@type="application/ld+json"]')->first()->text();

return json_decode($jsonLdSchema, true);
}
}
2 changes: 2 additions & 0 deletions app/Scraper/Manager.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
final class Manager
{
private $supportedSites = [
\Kami\Cocktail\Scraper\Sites\ACoupleCooks::class,
\Kami\Cocktail\Scraper\Sites\TuxedoNo2::class,
// \Kami\Cocktail\Scraper\Sites\Imbibe::class,
];

public function __construct(private readonly string $url)
Expand Down
Loading

0 comments on commit d7c253a

Please sign in to comment.