diff --git a/app/Enums/MALImportBehavior.php b/app/Enums/MALImportBehavior.php
new file mode 100644
index 000000000..6ba4f76ec
--- /dev/null
+++ b/app/Enums/MALImportBehavior.php
@@ -0,0 +1,15 @@
+user = $user;
$this->results = $results;
diff --git a/app/Http/Controllers/LibraryController.php b/app/Http/Controllers/LibraryController.php
index 1c4b94ffd..a3008a262 100644
--- a/app/Http/Controllers/LibraryController.php
+++ b/app/Http/Controllers/LibraryController.php
@@ -2,6 +2,7 @@
namespace App\Http\Controllers;
+use App\Enums\MALImportBehavior;
use App\Models\Anime;
use App\Enums\UserLibraryStatus;
use App\Helpers\JSONResult;
@@ -153,8 +154,11 @@ function malImport(MALImportRequest $request): JsonResponse
// Read XML file
$xmlContent = File::get($data['file']->getRealPath());
+ // Get import behavior
+ $behavior = MALImportBehavior::fromValue((int) $data['behavior']);
+
// Dispatch job
- dispatch(new ProcessMALImport($user, $xmlContent, $data['behavior']));
+ dispatch(new ProcessMALImport($user, $xmlContent, $behavior));
// Update last MAL import date for user
$user->last_mal_import_at = now();
diff --git a/app/Http/Requests/MALImportRequest.php b/app/Http/Requests/MALImportRequest.php
index 1af2881e1..07504a06d 100644
--- a/app/Http/Requests/MALImportRequest.php
+++ b/app/Http/Requests/MALImportRequest.php
@@ -2,6 +2,7 @@
namespace App\Http\Requests;
+use App\Enums\MALImportBehavior;
use Illuminate\Foundation\Http\FormRequest;
class MALImportRequest extends FormRequest
@@ -25,7 +26,7 @@ public function rules(): array
{
return [
'file' => ['required', 'file', 'mimes:xml', 'max:' . config('mal-import.max_xml_file_size')],
- 'behavior' => ['required', 'string', 'in:overwrite']
+ 'behavior' => ['required', 'integer', 'in:' . implode(',', MALImportBehavior::getValues())]
];
}
}
diff --git a/app/Jobs/ProcessMALImport.php b/app/Jobs/ProcessMALImport.php
index 048b08162..6893cf211 100644
--- a/app/Jobs/ProcessMALImport.php
+++ b/app/Jobs/ProcessMALImport.php
@@ -2,8 +2,11 @@
namespace App\Jobs;
+use App\Enums\MALImportBehavior;
use App\Models\Anime;
use App\Enums\UserLibraryStatus;
+use App\Models\AnimeRating;
+use App\Models\UserLibrary;
use App\Notifications\MALImportFinished;
use App\Models\User;
use Illuminate\Bus\Queueable;
@@ -40,9 +43,9 @@ class ProcessMALImport implements ShouldQueue
/**
* The behavior of the import action.
*
- * @var string
+ * @var MALImportBehavior
*/
- protected string $behavior;
+ protected MALImportBehavior $behavior;
/**
* The results of the import action.
@@ -59,9 +62,9 @@ class ProcessMALImport implements ShouldQueue
*
* @param User $user
* @param string $xmlContent
- * @param string $behavior
+ * @param MALImportBehavior $behavior
*/
- public function __construct(User $user, string $xmlContent, string $behavior)
+ public function __construct(User $user, string $xmlContent, MALImportBehavior $behavior)
{
$this->user = $user;
$this->xmlContent = $xmlContent;
@@ -74,8 +77,9 @@ public function __construct(User $user, string $xmlContent, string $behavior)
public function handle()
{
// Wipe current library if behavior is set to overwrite
- if ($this->behavior === 'overwrite') {
+ if ($this->behavior->value === MALImportBehavior::Overwrite) {
$this->user->library()->detach();
+ $this->user->animeRating()->delete();
}
// Create XML object
@@ -87,7 +91,7 @@ public function handle()
// Loop through the anime in the export file
foreach($json['anime'] as $anime) {
- $this->handleXMLFileAnime($anime['series_animedb_id'], $anime['my_status']);
+ $this->handleXMLFileAnime($anime['series_animedb_id'], $anime['my_status'], $anime['my_score']);
}
// Notify the user that the MAL import was finished
@@ -99,16 +103,18 @@ public function handle()
*
* @param int $malID
* @param string $malStatus
+ * @param int $malRating
*/
- protected function handleXMLFileAnime(int $malID, string $malStatus)
+ protected function handleXMLFileAnime(int $malID, string $malStatus, int $malRating)
{
// Try to find the Anime in our DB
- $animeMatch = Anime::where('mal_id', $malID)->first();
+ $anime = Anime::firstWhere('mal_id', $malID);
// If a match was found
- if ($animeMatch) {
- // Convert the MAL status to one of our own
+ if (!empty($anime)) {
+ // Convert the MAL data to our own
$status = $this->convertMALStatus($malStatus);
+ $rating = $this->convertMALRating($malRating);
// Status not found
if ($status === null) {
@@ -117,11 +123,25 @@ protected function handleXMLFileAnime(int $malID, string $malStatus)
}
// Add the anime to their library
- $this->user->library()->attach($animeMatch, ['status' => $status]);
-
- $this->registerSuccess($animeMatch->id, $malID, $status);
+ UserLibrary::updateOrCreate([
+ 'user_id' => $this->user->id,
+ 'anime_id' => $anime->id,
+ ], [
+ 'status' => $status
+ ]);
+
+ // Updated their anime score
+ AnimeRating::updateOrCreate([
+ 'user_id' => $this->user->id,
+ 'anime_id' => $anime->id,
+ ], [
+ 'rating' => $rating,
+ ]);
+
+ $this->registerSuccess($anime->id, $malID, $status, $rating);
+ } else {
+ $this->registerFailure($malID, 'MAL ID could not be found.');
}
- else $this->registerFailure($malID, 'MAL ID could not be found.');
}
/**
@@ -142,19 +162,36 @@ protected function convertMALStatus(string $malStatus): ?int
};
}
+ /**
+ * Converts and returns Kurozora specific rating.
+ *
+ * @param int $malRating
+ * @return int
+ */
+ protected function convertMALRating(int $malRating): int
+ {
+ if ($malRating == 0) {
+ return $malRating;
+ }
+
+ return round($malRating) * 0.5;
+ }
+
/**
* Registers a success in the import process.
*
* @param int $animeID
* @param int $malID
* @param string $status
+ * @param int $rating
*/
- protected function registerSuccess(int $animeID, int $malID, string $status)
+ protected function registerSuccess(int $animeID, int $malID, string $status, int $rating)
{
$this->results['successful'][] = [
'anime_id' => $animeID,
'mal_id' => $malID,
- 'status' => $status
+ 'status' => $status,
+ 'rating' => $rating,
];
}
diff --git a/app/Models/User.php b/app/Models/User.php
index 190873fb7..6e34b8453 100644
--- a/app/Models/User.php
+++ b/app/Models/User.php
@@ -143,6 +143,16 @@ class User extends Authenticatable implements HasMedia, MustVerifyEmail, Reacter
*/
protected string $profileImageCollectionName = 'profile';
+ /**
+ * Returns the anime ratings the user has.
+ *
+ * @return HasMany
+ */
+ public function animeRating(): HasMany
+ {
+ return $this->hasMany(AnimeRating::class);
+ }
+
/**
* Returns the associated feed messages for the user.
*
diff --git a/app/Notifications/MALImportFinished.php b/app/Notifications/MALImportFinished.php
index 817f3b7f4..3cfddd937 100644
--- a/app/Notifications/MALImportFinished.php
+++ b/app/Notifications/MALImportFinished.php
@@ -2,6 +2,7 @@
namespace App\Notifications;
+use App\Enums\MALImportBehavior;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
@@ -23,17 +24,17 @@ class MALImportFinished extends Notification implements ShouldQueue
/**
* The behavior used when importing.
*
- * @var string $behavior
+ * @var MALImportBehavior $behavior
*/
- private string $behavior;
+ private MALImportBehavior $behavior;
/**
* Create a new notification instance.
*
* @param array $results
- * @param string $behavior
+ * @param MALImportBehavior $behavior
*/
- public function __construct(array $results, string $behavior)
+ public function __construct(array $results, MALImportBehavior $behavior)
{
$this->results = $results;
$this->behavior = $behavior;
@@ -61,7 +62,7 @@ public function toDatabase(mixed $notifiable): array
return [
'successful_count' => count($this->results['successful']),
'failure_count' => count($this->results['failure']),
- 'behavior' => $this->behavior
+ 'behavior' => $this->behavior->description
];
}
diff --git a/config/app.php b/config/app.php
index 7d2e1ac29..c163d25b5 100644
--- a/config/app.php
+++ b/config/app.php
@@ -38,7 +38,7 @@
| or any other location as required by the application or its packages.
*/
- 'version' => '1.2.0-alpha.89',
+ 'version' => '1.2.0-alpha.91',
/*
|--------------------------------------------------------------------------
diff --git a/config/mal-import.php b/config/mal-import.php
index 633986eec..baf447240 100644
--- a/config/mal-import.php
+++ b/config/mal-import.php
@@ -10,11 +10,11 @@
| This option controls the cooldown in days for users between importing
| a MAL export file.
|
- | Default: 7
+ | Default: 3
|
*/
- 'cooldown_in_days' => 7,
+ 'cooldown_in_days' => 3,
/*
|--------------------------------------------------------------------------
@@ -24,10 +24,10 @@
| This option controls the maximum allowed file size for a MAL export file
| in kilobytes.
|
- | Default: 5000
+ | Default: 10000
|
*/
- 'max_xml_file_size' => 5000
+ 'max_xml_file_size' => 10000
];
diff --git a/database/migrations/2018_08_22_154146_create_anime_ratings_table.php b/database/migrations/2018_08_22_154146_create_anime_ratings_table.php
index 26095b439..faff62321 100644
--- a/database/migrations/2018_08_22_154146_create_anime_ratings_table.php
+++ b/database/migrations/2018_08_22_154146_create_anime_ratings_table.php
@@ -25,6 +25,9 @@ public function up()
});
Schema::table(AnimeRating::TABLE_NAME, function(Blueprint $table) {
+ // Set unique key constraints
+ $table->unique(['anime_id', 'user_id']);
+
// Set foreign key constraints
$table->foreign('anime_id')->references('id')->on(Anime::TABLE_NAME)->onDelete('cascade');
$table->foreign('user_id')->references('id')->on(User::TABLE_NAME)->onDelete('cascade');
diff --git a/public/openapi.json b/public/openapi.json
index d12436462..881cec276 100644
--- a/public/openapi.json
+++ b/public/openapi.json
@@ -3491,12 +3491,13 @@
"description": "The lock status used to lock or unlock a thread.\n- 0 = unlock\n- 1 = lock"
},
"MALImportBehavior": {
- "type": "string",
+ "type": "integer",
"enum": [
- "overwrite"
+ 0,
+ 1
],
- "default": "overwrite",
- "description": "The set of available MAL import behavior types."
+ "default": 1,
+ "description": "The set of available MAL import behavior types.\n- 0 = overwrite\n- 1 = merge"
},
"NotificationReadStatus": {
"type": "integer",
diff --git a/tests/API/MALImportTest.php b/tests/API/MALImportTest.php
new file mode 100644
index 000000000..6655baf26
--- /dev/null
+++ b/tests/API/MALImportTest.php
@@ -0,0 +1,258 @@
+
+
+ 6076
+
+ OVA
+ 1
+ 0
+ 1
+ 0000-00-00
+ 0000-00-00
+
+ 10
+
+ 0.00
+ Completed
+
+ 0
+
+ LOW
+
+ 0
+ 0
+ 1
+ default
+ 0
+
+
+
+ 2928
+
+ OVA
+ 1
+ 0
+ 1
+ 0000-00-00
+ 0000-00-00
+
+ 10
+
+ 0.00
+ Dropped
+
+ 0
+
+ HIGH
+
+ 0
+ 0
+ 0
+ default
+ 0
+
+
+
+ 3269
+
+ Movie
+ 1
+ 0
+ 1
+ 0000-00-00
+ 0000-00-00
+
+ 10
+
+ 0.00
+ Plan to Watch
+
+ 0
+
+ HIGH
+
+ 0
+ 0
+ 0
+ default
+ 0
+
+
+
+ 4469
+
+ Special
+ 1
+ 0
+ 1
+ 0000-00-00
+ 0000-00-00
+
+ 10
+
+ 0.00
+ On-Hold
+
+ 0
+
+ HIGH
+
+ 0
+ 0
+ 0
+ default
+ 0
+
+
+
+ 454
+
+ OVA
+ 1
+ 0
+ 1
+ 0000-00-00
+ 0000-00-00
+
+ 10
+
+ 0.00
+ Completed
+
+ 0
+
+ HIGH
+
+ 0
+ 0
+ 0
+ default
+ 0
+
+
+
+ 1143
+
+ Special
+ 1
+ 0
+ 1
+ 0000-00-00
+ 0000-00-00
+
+ 10
+
+ 0.00
+ Watching
+
+ 0
+
+ HIGH
+
+ 0
+ 0
+ 0
+ default
+ 0
+
+
+ XML;
+
+ /**
+ * User can import MAL library with overwrite behavior.
+ *
+ * @return void
+ * @test
+ */
+ function user_can_import_mal_library_with_overwrite_behavior()
+ {
+ // Attach anime with id 21
+ $anime = Anime::firstWhere('mal_id', 21);
+ $this->user->library()->attach($anime, ['status' => 'Watching']);
+
+ // Prepare import file
+ $uploadFile = UploadedFile::fake()->createWithContent('animelist_1623616958_-_3667065.xml', self::$xmlContent);
+
+ // Make sure the anime has been attached to the user
+ $this->assertEquals(1, $this->user->library()->count());
+
+ // Expect a job is dispatched
+ Notification::fake();
+
+ // Request import
+ $response = $this->auth()->json('POST', 'v1/me/library/mal-import', [
+ 'file' => $uploadFile,
+ 'behavior' => MALImportBehavior::Overwrite,
+ ]);
+ $response->assertSuccessfulAPIResponse();
+
+ // Assert notification was sent
+ Notification::hasSent($this->user, MALImportFinished::class);
+
+ // Assert anime has been imported in user's library
+ $this->assertEquals(6, $this->user->library()->count());
+
+ // Assert the anime we added in the beginning is gone
+ $this->assertNull($this->user->library->firstWhere('mal_id', 21));
+ }
+
+ /**
+ * User can import MAL library with merge behavior.
+ *
+ * @test
+ */
+ function user_can_import_mal_library_with_merge_behavior()
+ {
+ // Attach anime with id 21
+ $anime = Anime::firstWhere('mal_id', 21);
+ $this->user->library()->attach($anime, ['status' => 'Watching']);
+
+ // Prepare import file
+ $uploadFile = UploadedFile::fake()->createWithContent('animelist_1623616958_-_3667065.xml', self::$xmlContent);
+
+ // Make sure the anime has been attached to the user
+ $this->assertEquals(1, $this->user->library()->count());
+
+ // Expect a job is dispatched
+ Notification::fake();
+
+ // Request import
+ $response = $this->auth()->json('POST', 'v1/me/library/mal-import', [
+ 'file' => $uploadFile,
+ 'behavior' => MALImportBehavior::Merge,
+ ]);
+ $response->assertSuccessfulAPIResponse();
+
+ // Assert notification was sent
+ Notification::hasSent($this->user, MALImportFinished::class);
+
+ // Assert anime has been imported in user's library
+ $this->assertEquals(7, $this->user->library()->count());
+
+ // Assert the anime we added in the beginning is gone
+ $this->assertNotNull($this->user->library->firstWhere('mal_id', 21));
+ }
+}
diff --git a/tests/TestCase.php b/tests/TestCase.php
index 0bf2c719f..6d6f28eb2 100644
--- a/tests/TestCase.php
+++ b/tests/TestCase.php
@@ -10,6 +10,7 @@
use Illuminate\Testing\TestResponse;
use Spatie\Snapshots\MatchesSnapshots;
use Tests\Traits\ProvidesTestAnime;
+use Tests\Traits\ProvidesTestMultipleAnime;
use Tests\Traits\ProvidesTestUser;
abstract class TestCase extends BaseTestCase
@@ -60,6 +61,9 @@ protected function setUpTraits(): array
if (isset($uses[ProvidesTestAnime::class])) {
$this->initializeTestAnime();
}
+ if (isset($uses[ProvidesTestMultipleAnime::class])) {
+ $this->initializeTestMultipleAnime();
+ }
return $uses;
}
diff --git a/tests/Traits/ProvidesTestMultipleAnime.php b/tests/Traits/ProvidesTestMultipleAnime.php
new file mode 100644
index 000000000..270676473
--- /dev/null
+++ b/tests/Traits/ProvidesTestMultipleAnime.php
@@ -0,0 +1,36 @@
+malIds as $malId) {
+ Anime::factory()->create([
+ 'mal_id' => $malId
+ ]);
+ }
+ }
+}