diff --git a/.sqlx/query-8fce2d85078e69b81b084aabdc16e99bd7a609452c13502ae6287c489b512797.json b/.sqlx/query-8fce2d85078e69b81b084aabdc16e99bd7a609452c13502ae6287c489b512797.json
new file mode 100644
index 0000000..9b02faf
--- /dev/null
+++ b/.sqlx/query-8fce2d85078e69b81b084aabdc16e99bd7a609452c13502ae6287c489b512797.json
@@ -0,0 +1,68 @@
+{
+ "db_name": "SQLite",
+ "query": "\n SELECT m.id AS \"id!: i64\",\n m.tmdb_id AS \"tmdb_id!: i64\",\n m.title AS \"title!: String\",\n m.year,\n m.original_language,\n m.search_attempts AS \"search_attempts!: i64\",\n m.last_searched_at,\n m.digital_release,\n m.metadata_refreshed_at\n FROM movies m\n WHERE m.id = ?\n AND m.wanted = 1\n AND m.blocked = 0\n AND NOT EXISTS (\n SELECT 1 FROM media_files f\n WHERE f.owner_kind = 'movie' AND f.owner_id = m.id\n )\n AND NOT EXISTS (\n SELECT 1 FROM grabs g\n WHERE g.target_kind = 'movie' AND g.target_id = m.id\n AND g.state IN ('sent', 'downloaded', 'imported')\n )\n ",
+ "describe": {
+ "columns": [
+ {
+ "name": "id!: i64",
+ "ordinal": 0,
+ "type_info": "Integer"
+ },
+ {
+ "name": "tmdb_id!: i64",
+ "ordinal": 1,
+ "type_info": "Integer"
+ },
+ {
+ "name": "title!: String",
+ "ordinal": 2,
+ "type_info": "Text"
+ },
+ {
+ "name": "year",
+ "ordinal": 3,
+ "type_info": "Integer"
+ },
+ {
+ "name": "original_language",
+ "ordinal": 4,
+ "type_info": "Text"
+ },
+ {
+ "name": "search_attempts!: i64",
+ "ordinal": 5,
+ "type_info": "Integer"
+ },
+ {
+ "name": "last_searched_at",
+ "ordinal": 6,
+ "type_info": "Text"
+ },
+ {
+ "name": "digital_release",
+ "ordinal": 7,
+ "type_info": "Text"
+ },
+ {
+ "name": "metadata_refreshed_at",
+ "ordinal": 8,
+ "type_info": "Text"
+ }
+ ],
+ "parameters": {
+ "Right": 1
+ },
+ "nullable": [
+ false,
+ false,
+ false,
+ true,
+ true,
+ false,
+ true,
+ true,
+ true
+ ]
+ },
+ "hash": "8fce2d85078e69b81b084aabdc16e99bd7a609452c13502ae6287c489b512797"
+}
diff --git a/.sqlx/query-a0d7296eced171dd9ce02faadfaea5bc67e37123d7e30b18b575f0dd566952ae.json b/.sqlx/query-a0d7296eced171dd9ce02faadfaea5bc67e37123d7e30b18b575f0dd566952ae.json
new file mode 100644
index 0000000..1f2a2bf
--- /dev/null
+++ b/.sqlx/query-a0d7296eced171dd9ce02faadfaea5bc67e37123d7e30b18b575f0dd566952ae.json
@@ -0,0 +1,50 @@
+{
+ "db_name": "SQLite",
+ "query": "\n SELECT r.id AS \"id!: i64\",\n r.indexer_id AS \"indexer_id!: i64\",\n r.guid AS \"guid!: String\",\n r.name AS \"name!: String\",\n r.download_url AS \"download_url!: String\",\n CAST(COALESCE(r.score, 0) AS INTEGER) AS \"score!: i64\"\n FROM releases r\n JOIN movie_releases mr ON mr.release_id = r.id\n WHERE mr.movie_id = ? AND r.id = ? AND r.verdict IN ('eligible', 'waived')\n ",
+ "describe": {
+ "columns": [
+ {
+ "name": "id!: i64",
+ "ordinal": 0,
+ "type_info": "Integer"
+ },
+ {
+ "name": "indexer_id!: i64",
+ "ordinal": 1,
+ "type_info": "Integer"
+ },
+ {
+ "name": "guid!: String",
+ "ordinal": 2,
+ "type_info": "Text"
+ },
+ {
+ "name": "name!: String",
+ "ordinal": 3,
+ "type_info": "Text"
+ },
+ {
+ "name": "download_url!: String",
+ "ordinal": 4,
+ "type_info": "Text"
+ },
+ {
+ "name": "score!: i64",
+ "ordinal": 5,
+ "type_info": "Null"
+ }
+ ],
+ "parameters": {
+ "Right": 2
+ },
+ "nullable": [
+ true,
+ false,
+ false,
+ false,
+ false,
+ null
+ ]
+ },
+ "hash": "a0d7296eced171dd9ce02faadfaea5bc67e37123d7e30b18b575f0dd566952ae"
+}
diff --git a/.sqlx/query-ce34d0f329c6e3f8ec2df1b9c85502186ff6fddddd98cc15423fcd41ca308a04.json b/.sqlx/query-ce34d0f329c6e3f8ec2df1b9c85502186ff6fddddd98cc15423fcd41ca308a04.json
new file mode 100644
index 0000000..75262e1
--- /dev/null
+++ b/.sqlx/query-ce34d0f329c6e3f8ec2df1b9c85502186ff6fddddd98cc15423fcd41ca308a04.json
@@ -0,0 +1,20 @@
+{
+ "db_name": "SQLite",
+ "query": "SELECT title FROM movies WHERE id = ?",
+ "describe": {
+ "columns": [
+ {
+ "name": "title",
+ "ordinal": 0,
+ "type_info": "Text"
+ }
+ ],
+ "parameters": {
+ "Right": 1
+ },
+ "nullable": [
+ false
+ ]
+ },
+ "hash": "ce34d0f329c6e3f8ec2df1b9c85502186ff6fddddd98cc15423fcd41ca308a04"
+}
diff --git a/.sqlx/query-e6274f081ce0751331e78f7fec573e088d08e3683fc60e84ff325e95fe42d803.json b/.sqlx/query-e6274f081ce0751331e78f7fec573e088d08e3683fc60e84ff325e95fe42d803.json
new file mode 100644
index 0000000..91b5055
--- /dev/null
+++ b/.sqlx/query-e6274f081ce0751331e78f7fec573e088d08e3683fc60e84ff325e95fe42d803.json
@@ -0,0 +1,12 @@
+{
+ "db_name": "SQLite",
+ "query": "UPDATE movies SET search_attempts = 0, last_searched_at = NULL,\n updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')\n WHERE id = ?",
+ "describe": {
+ "columns": [],
+ "parameters": {
+ "Right": 1
+ },
+ "nullable": []
+ },
+ "hash": "e6274f081ce0751331e78f7fec573e088d08e3683fc60e84ff325e95fe42d803"
+}
diff --git a/crates/arr-daemon/src/grab.rs b/crates/arr-daemon/src/grab.rs
index 598426a..eecc8f8 100644
--- a/crates/arr-daemon/src/grab.rs
+++ b/crates/arr-daemon/src/grab.rs
@@ -373,6 +373,64 @@ impl GrabAction {
)
.await
}
+
+ /// The manual trigger (§6.2, §9.3, issue #107): reset the backoff so
+ /// `search_due` cannot skip the title, then run the same search-and-grab
+ /// path as a tick, scoped to this one movie instead of the tick's
+ /// `MOVIES_PER_TICK` batch.
+ pub(crate) async fn search_now(
+ &self,
+ database: &Db,
+ movie_id: i64,
+ ) -> Result