backuparr 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- backuparr-0.1.0/LICENSE +21 -0
- backuparr-0.1.0/PKG-INFO +98 -0
- backuparr-0.1.0/README.md +64 -0
- backuparr-0.1.0/backuparr/api_stuff.py +23 -0
- backuparr-0.1.0/backuparr/backup.py +60 -0
- backuparr-0.1.0/backuparr/functions.py +149 -0
- backuparr-0.1.0/backuparr/main.py +26 -0
- backuparr-0.1.0/backuparr/restore.py +109 -0
- backuparr-0.1.0/backuparr.egg-info/PKG-INFO +98 -0
- backuparr-0.1.0/backuparr.egg-info/SOURCES.txt +14 -0
- backuparr-0.1.0/backuparr.egg-info/dependency_links.txt +1 -0
- backuparr-0.1.0/backuparr.egg-info/entry_points.txt +2 -0
- backuparr-0.1.0/backuparr.egg-info/requires.txt +2 -0
- backuparr-0.1.0/backuparr.egg-info/top_level.txt +1 -0
- backuparr-0.1.0/pyproject.toml +16 -0
- backuparr-0.1.0/setup.cfg +4 -0
backuparr-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 DanielMayhan
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
backuparr-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: backuparr
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A lightweight, efficient way to backup your media library without duplicating large video files.
|
|
5
|
+
Author: Daniel Mayhan
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 DanielMayhan
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Requires-Python: >=3.14
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
License-File: LICENSE
|
|
31
|
+
Requires-Dist: requests
|
|
32
|
+
Requires-Dist: python-dotenv
|
|
33
|
+
Dynamic: license-file
|
|
34
|
+
|
|
35
|
+
# BackupArr
|
|
36
|
+
|
|
37
|
+
> [!NOTE]
|
|
38
|
+
> **AI-Generated Documentation:** This README was drafted with the assistance of AI, but none of the Code in this Project was written by AI.
|
|
39
|
+
|
|
40
|
+
Welcome to the **BackupArr** repository. This project is currently in its early development stages (**Pre-Alpha**) and aims to provide a lightweight, efficient way to "back up" your media library without duplicating large video files.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## The Concept
|
|
45
|
+
Traditional backups for media libraries are often prohibitively expensive due to massive file sizes. **BackupArr** changes the paradigm: instead of backing up the media files themselves, this tool maps your library metadata back to its source. In the event of data loss, you can restore the library state to your download client to re-acquire the media from the swarm.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Project Status: Alpha
|
|
50
|
+
**Warning:** This software is experimental and **Work in Progress**. It is not recommended for production use. Any and all code are subject to radical changes as development progresses.
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Getting Started (Development)
|
|
55
|
+
|
|
56
|
+
1. **Clone the repository:**
|
|
57
|
+
```bash
|
|
58
|
+
git clone https://github.com/DanielMayhan/BackupArr.git
|
|
59
|
+
cd BackupArr
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
2. **Configure Environment:**
|
|
63
|
+
Copy the sample configuration and add your Radarr API key & Url.
|
|
64
|
+
```bash
|
|
65
|
+
cp .env.example .env
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Usage
|
|
71
|
+
* **Command Structure:**
|
|
72
|
+
```bash
|
|
73
|
+
python3 main.py [backup|restore] [radarr|sonarr] [filename].json
|
|
74
|
+
* **Example Command:**
|
|
75
|
+
```bash
|
|
76
|
+
python3 main.py backup radarr radarr_backup.json
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Roadmap
|
|
82
|
+
These are features and code changes that are either currently being worked on or are planned to be implemented later!
|
|
83
|
+
* Code Cleanup
|
|
84
|
+
* Unification of all text in-/outputs
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Contributing
|
|
90
|
+
We welcome all input during these early stages!
|
|
91
|
+
* Open an **Issue** to discuss new ideas or report bugs.
|
|
92
|
+
* Submit a **Pull Request** to help with early-stage logic.
|
|
93
|
+
* Check the **Discussions** tab to help define the project roadmap.
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## License
|
|
98
|
+
This project is licensed under the [MIT License](LICENSE).
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# BackupArr
|
|
2
|
+
|
|
3
|
+
> [!NOTE]
|
|
4
|
+
> **AI-Generated Documentation:** This README was drafted with the assistance of AI, but none of the Code in this Project was written by AI.
|
|
5
|
+
|
|
6
|
+
Welcome to the **BackupArr** repository. This project is currently in its early development stages (**Pre-Alpha**) and aims to provide a lightweight, efficient way to "back up" your media library without duplicating large video files.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## The Concept
|
|
11
|
+
Traditional backups for media libraries are often prohibitively expensive due to massive file sizes. **BackupArr** changes the paradigm: instead of backing up the media files themselves, this tool maps your library metadata back to its source. In the event of data loss, you can restore the library state to your download client to re-acquire the media from the swarm.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Project Status: Alpha
|
|
16
|
+
**Warning:** This software is experimental and **Work in Progress**. It is not recommended for production use. Any and all code are subject to radical changes as development progresses.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Getting Started (Development)
|
|
21
|
+
|
|
22
|
+
1. **Clone the repository:**
|
|
23
|
+
```bash
|
|
24
|
+
git clone https://github.com/DanielMayhan/BackupArr.git
|
|
25
|
+
cd BackupArr
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
2. **Configure Environment:**
|
|
29
|
+
Copy the sample configuration and add your Radarr API key & Url.
|
|
30
|
+
```bash
|
|
31
|
+
cp .env.example .env
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Usage
|
|
37
|
+
* **Command Structure:**
|
|
38
|
+
```bash
|
|
39
|
+
python3 main.py [backup|restore] [radarr|sonarr] [filename].json
|
|
40
|
+
* **Example Command:**
|
|
41
|
+
```bash
|
|
42
|
+
python3 main.py backup radarr radarr_backup.json
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Roadmap
|
|
48
|
+
These are features and code changes that are either currently being worked on or are planned to be implemented later!
|
|
49
|
+
* Code Cleanup
|
|
50
|
+
* Unification of all text in-/outputs
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Contributing
|
|
56
|
+
We welcome all input during these early stages!
|
|
57
|
+
* Open an **Issue** to discuss new ideas or report bugs.
|
|
58
|
+
* Submit a **Pull Request** to help with early-stage logic.
|
|
59
|
+
* Check the **Discussions** tab to help define the project roadmap.
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## License
|
|
64
|
+
This project is licensed under the [MIT License](LICENSE).
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from enum import StrEnum
|
|
4
|
+
from dotenv import load_dotenv
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
load_dotenv()
|
|
9
|
+
|
|
10
|
+
class radarr(StrEnum):
|
|
11
|
+
apiKey = str(os.getenv("RADARR_APIKEY"))
|
|
12
|
+
baseUrl = str(os.getenv("RADARR_URL"))
|
|
13
|
+
movieListUrl = baseUrl + "/api/v3/movie"
|
|
14
|
+
qualityProfileUrl = baseUrl + "/api/v3/qualityprofile"
|
|
15
|
+
rootFolderUrl = baseUrl + "/api/v3/rootfolder"
|
|
16
|
+
|
|
17
|
+
class sonarr(StrEnum):
|
|
18
|
+
apiKey = str(os.getenv("SONARR_APIKEY"))
|
|
19
|
+
baseUrl = str(os.getenv("SONARR_URL"))
|
|
20
|
+
seriesListUrl = baseUrl + "/api/v3/series"
|
|
21
|
+
episodeFileUrl = baseUrl + "/api/v3/episodefile"
|
|
22
|
+
rootFolderUrl = baseUrl + "/api/v3/rootfolder"
|
|
23
|
+
qualityProfileUrl = baseUrl + "/api/v3/qualityprofile"
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import sys
|
|
3
|
+
import requests
|
|
4
|
+
|
|
5
|
+
from backuparr import api_stuff as api
|
|
6
|
+
from backuparr import functions
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def run(app, filename):
|
|
10
|
+
## Resolving Filename
|
|
11
|
+
filename = functions.resolveFilename(filename)
|
|
12
|
+
|
|
13
|
+
## Attempt Connection
|
|
14
|
+
movieData = {}
|
|
15
|
+
match app:
|
|
16
|
+
case "radarr":
|
|
17
|
+
movieData = functions.attemptConnection(api.radarr.movieListUrl, api.radarr.apiKey)
|
|
18
|
+
case "sonarr":
|
|
19
|
+
movieData = functions.attemptConnection(api.sonarr.seriesListUrl, api.sonarr.apiKey)
|
|
20
|
+
|
|
21
|
+
## Display found movies
|
|
22
|
+
len_data = len(movieData)
|
|
23
|
+
if len_data == 1:
|
|
24
|
+
print(len_data, "movie/series have been found.")
|
|
25
|
+
elif len_data == 0:
|
|
26
|
+
print("No movie/series have been found.")
|
|
27
|
+
print("Exiting...")
|
|
28
|
+
sys.exit("No movies/series found.")
|
|
29
|
+
else:
|
|
30
|
+
print(len_data, "movies/series have been found.")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
## Making and writing data to JSON file
|
|
34
|
+
jsondata = {}
|
|
35
|
+
for i in range(len_data):
|
|
36
|
+
match app:
|
|
37
|
+
case "radarr":
|
|
38
|
+
jsondata[str(movieData[i]["tmdbId"])] = functions.makeJsonData(i, movieData)
|
|
39
|
+
case "sonarr":
|
|
40
|
+
quality = -1
|
|
41
|
+
if int(movieData[i]["statistics"]["episodeFileCount"]) > 0:
|
|
42
|
+
req = requests.get(api.sonarr.episodeFileUrl + "?seriesId=" + str(movieData[i]["id"]), headers={"x-api-key": api.sonarr.apiKey}).json()
|
|
43
|
+
for j in range(len(req)):
|
|
44
|
+
try:
|
|
45
|
+
quality = int(req[j]["quality"]["quality"]["resolution"])
|
|
46
|
+
break
|
|
47
|
+
except Exception as e:
|
|
48
|
+
print("Resolution not found, trying another file...")
|
|
49
|
+
jsondata[str(movieData[i]["tvdbId"])] = functions.makeSonarrData(i, movieData, quality)
|
|
50
|
+
|
|
51
|
+
print("Writing Data for: " + str(movieData[i]["title"]))
|
|
52
|
+
|
|
53
|
+
with open(filename, "w", encoding="utf-8") as f:
|
|
54
|
+
json.dump(jsondata, f, indent=4, ensure_ascii=False, sort_keys=True)
|
|
55
|
+
|
|
56
|
+
print("Data has been writen to:", filename)
|
|
57
|
+
print("Exiting...")
|
|
58
|
+
|
|
59
|
+
if __name__ == "__main__":
|
|
60
|
+
run()
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import sys
|
|
3
|
+
import requests
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from requests.exceptions import HTTPError, Timeout, RequestException
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def attemptConnection(connectionUrl, apiKey):
|
|
10
|
+
while True:
|
|
11
|
+
(connected, jsonData) = getJsonDataFromUrl(connectionUrl, apiKey)
|
|
12
|
+
if connected: return jsonData
|
|
13
|
+
while True:
|
|
14
|
+
choice = input("Connection failed, make sure the URL is valid and accessible. (y)Reconnect | (n) Exit: ").lower().strip()
|
|
15
|
+
if choice == "n": sys.exit("User terminated the process")
|
|
16
|
+
elif choice == "y":
|
|
17
|
+
print("Reconnecting...")
|
|
18
|
+
break
|
|
19
|
+
else: print("Invalid input. Use: y/n")
|
|
20
|
+
|
|
21
|
+
def getJsonDataFromUrl(connectionUrl, apiKey):
|
|
22
|
+
noApiUrl = connectionUrl.split("?apiKey=")[0]
|
|
23
|
+
try:
|
|
24
|
+
print("Connecting to " + noApiUrl)
|
|
25
|
+
headers = {"x-api-key" : apiKey}
|
|
26
|
+
response = requests.get(connectionUrl, headers=headers, timeout=10)
|
|
27
|
+
if response.status_code == 401:
|
|
28
|
+
print("Error: 401 Request unauthorized. @:", noApiUrl)
|
|
29
|
+
return False, ""
|
|
30
|
+
print("Connection established.")
|
|
31
|
+
return True, response.json()
|
|
32
|
+
except Timeout:
|
|
33
|
+
print("Error: The request timed out. This URL might be down or slow. @:", noApiUrl)
|
|
34
|
+
return False, ""
|
|
35
|
+
except ConnectionError:
|
|
36
|
+
print("Error: Failed to connect to this URL. Check your URL or network. @:", noApiUrl)
|
|
37
|
+
return False, ""
|
|
38
|
+
except HTTPError as e:
|
|
39
|
+
print(f"HTTP Error: {e}\n@:", noApiUrl)
|
|
40
|
+
return False, ""
|
|
41
|
+
except RequestException as e:
|
|
42
|
+
print(f"An ambiguous error occurred: {e}\n@:", noApiUrl)
|
|
43
|
+
return False, ""
|
|
44
|
+
except ValueError:
|
|
45
|
+
print("Error: Successfully connected, but received invalid JSON. @:", noApiUrl)
|
|
46
|
+
return False, ""
|
|
47
|
+
|
|
48
|
+
def makeJsonData(index, data):
|
|
49
|
+
try:
|
|
50
|
+
if data[index].get("movieFile") is not None: quality = data[index]["movieFile"]["quality"]["quality"]["resolution"]
|
|
51
|
+
else: quality = -1
|
|
52
|
+
|
|
53
|
+
jsonData = {
|
|
54
|
+
"title": str(data[index]["title"]),
|
|
55
|
+
"tmdbId": int(data[index]["tmdbId"]),
|
|
56
|
+
"monitored": bool(data[index]["monitored"]),
|
|
57
|
+
"quality": int(quality)
|
|
58
|
+
}
|
|
59
|
+
return jsonData
|
|
60
|
+
except Exception as e:
|
|
61
|
+
print("KeyError: Important Data not found!")
|
|
62
|
+
print(str(e))
|
|
63
|
+
sys.exit()
|
|
64
|
+
|
|
65
|
+
def makeSonarrData(index, data, quality):
|
|
66
|
+
try:
|
|
67
|
+
jsonData = {
|
|
68
|
+
"title": str(data[index]["title"]),
|
|
69
|
+
"tvdbId": int(data[index]["tvdbId"]),
|
|
70
|
+
"monitored": bool(data[index]["monitored"]),
|
|
71
|
+
"quality": int(quality)
|
|
72
|
+
}
|
|
73
|
+
return jsonData
|
|
74
|
+
except Exception as e:
|
|
75
|
+
print("KeyError: Important Data not found!")
|
|
76
|
+
print(str(e))
|
|
77
|
+
sys.exit()
|
|
78
|
+
|
|
79
|
+
def getNumUserInput(lastnum):
|
|
80
|
+
while True:
|
|
81
|
+
num = input("Enter choice, default [0]: ").strip()
|
|
82
|
+
try:
|
|
83
|
+
if not num: return 0
|
|
84
|
+
num = int(num)
|
|
85
|
+
if int(num) <= lastnum: return int(num)
|
|
86
|
+
else: print(str(num) + " is not a integer, or a valid input...")
|
|
87
|
+
|
|
88
|
+
except ValueError:
|
|
89
|
+
print(str(num) + " is not a valid input...")
|
|
90
|
+
|
|
91
|
+
def resolveFilename(path):
|
|
92
|
+
filename = Path(path).resolve()
|
|
93
|
+
try:
|
|
94
|
+
filename.parent.mkdir(parents=True, exist_ok=True)
|
|
95
|
+
return filename
|
|
96
|
+
except Exception as e:
|
|
97
|
+
print("An Error occurred: " + str(e))
|
|
98
|
+
|
|
99
|
+
def postJsonData(connectionUrl, apiKey, jsonDataList):
|
|
100
|
+
statusDict = {}
|
|
101
|
+
print("Attempting to post data for " + str(len(jsonDataList)) + " entries...")
|
|
102
|
+
for index, item in enumerate(jsonDataList):
|
|
103
|
+
try:
|
|
104
|
+
print("Importing: " + item["title"])
|
|
105
|
+
req_response = requests.post(connectionUrl, headers={"x-api-key" : apiKey}, json=item)
|
|
106
|
+
if not req_response.status_code == 201:
|
|
107
|
+
statusDict[index] = req_response
|
|
108
|
+
print("Code: " + str(req_response.status_code) + " | " + str(req_response.elapsed.total_seconds()) + "s")
|
|
109
|
+
except Exception as e:
|
|
110
|
+
print("An Error occurred: " + str(e))
|
|
111
|
+
|
|
112
|
+
while True:
|
|
113
|
+
if len(statusDict) == 0: return
|
|
114
|
+
|
|
115
|
+
print("Successfully posted data for " + str(len(jsonDataList) - len(statusDict)) + " entries...")
|
|
116
|
+
|
|
117
|
+
choice = input("Do you want to retry all entries that didn't return 201 Created? Retry(r) | List(l) | Default: Exit(e): ").strip().lower()
|
|
118
|
+
if not choice or choice == "e":
|
|
119
|
+
sys.exit("User terminated process.")
|
|
120
|
+
elif choice == "l":
|
|
121
|
+
for j, resp in statusDict.items():
|
|
122
|
+
print(f"----- {jsonDataList[j]["title"]} -----")
|
|
123
|
+
print(f"Code: {resp.status_code}")
|
|
124
|
+
print(f"Response:\n{json.dumps(resp.json(), indent=4)}")
|
|
125
|
+
print("-" * 100)
|
|
126
|
+
|
|
127
|
+
while True:
|
|
128
|
+
choice2 = input("Do you want to retry importing? (y/n): ").strip().lower()
|
|
129
|
+
if choice2 == "y": break
|
|
130
|
+
elif choice2 == "n": sys.exit("User terminated process.")
|
|
131
|
+
else: print("Invalid Choice, retrying...")
|
|
132
|
+
elif choice == "r": print("Retrying...")
|
|
133
|
+
else:
|
|
134
|
+
print("Invalid Choice, retrying...")
|
|
135
|
+
continue
|
|
136
|
+
|
|
137
|
+
toDelete = []
|
|
138
|
+
for k, resp in statusDict.items():
|
|
139
|
+
print("Importing: " + jsonDataList[k]["title"])
|
|
140
|
+
retry_resp = requests.post(connectionUrl, headers={"x-api-key" : apiKey}, json=jsonDataList[k])
|
|
141
|
+
print("Code: " + str(retry_resp.status_code) + " | " + str(retry_resp.elapsed.total_seconds()) + "s")
|
|
142
|
+
if retry_resp.status_code == 201:
|
|
143
|
+
print(f"Successfully posted data for: {jsonDataList[k]["title"]}")
|
|
144
|
+
toDelete.append(k)
|
|
145
|
+
else:
|
|
146
|
+
print(f"Still unable to import: {jsonDataList[k]["title"]}")
|
|
147
|
+
|
|
148
|
+
for l in toDelete:
|
|
149
|
+
del statusDict[l]
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from backuparr import backup, restore
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def run():
|
|
6
|
+
if len(sys.argv) != 4:
|
|
7
|
+
print("Usage: backuparr [backup|restore] [radarr|sonarr] [filename]")
|
|
8
|
+
return
|
|
9
|
+
|
|
10
|
+
mode = str(sys.argv[1].lower().strip())
|
|
11
|
+
app = str(sys.argv[2].lower().strip())
|
|
12
|
+
filename = str(sys.argv[3])
|
|
13
|
+
|
|
14
|
+
if (app != "radarr") and (app != "sonarr"):
|
|
15
|
+
print(app)
|
|
16
|
+
sys.exit("Argument Error, 2. Argument must be 'radarr' or 'sonarr'")
|
|
17
|
+
|
|
18
|
+
if mode == "backup":
|
|
19
|
+
backup.run(app, filename)
|
|
20
|
+
elif mode == "restore":
|
|
21
|
+
restore.run(app, filename)
|
|
22
|
+
else:
|
|
23
|
+
print("Argument Error, Usage: backuparr [backup|restore] [radarr|sonarr] [filename]")
|
|
24
|
+
|
|
25
|
+
if __name__ == "__main__":
|
|
26
|
+
run()
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
from backuparr import api_stuff as api
|
|
5
|
+
from backuparr import functions
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def run(app, path):
|
|
9
|
+
## Resolving Filename
|
|
10
|
+
filename = functions.resolveFilename(path)
|
|
11
|
+
|
|
12
|
+
## Required API Calls
|
|
13
|
+
rootFolder = ""
|
|
14
|
+
qualityProfiles = ""
|
|
15
|
+
match app:
|
|
16
|
+
case "radarr":
|
|
17
|
+
rootFolder = functions.attemptConnection(api.radarr.rootFolderUrl, api.radarr.apiKey)
|
|
18
|
+
qualityProfiles = functions.attemptConnection(api.radarr.qualityProfileUrl, api.radarr.apiKey)
|
|
19
|
+
case "sonarr":
|
|
20
|
+
rootFolder = functions.attemptConnection(api.sonarr.rootFolderUrl, api.sonarr.apiKey)
|
|
21
|
+
qualityProfiles = functions.attemptConnection(api.sonarr.qualityProfileUrl, api.sonarr.apiKey)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
## Selecting Root Folder
|
|
25
|
+
if len(rootFolder) == 0:
|
|
26
|
+
sys.exit("No root folder found, please add a root folder.")
|
|
27
|
+
|
|
28
|
+
elif len(rootFolder) == 1:
|
|
29
|
+
selectedRootFolderPath = rootFolder[0]["path"]
|
|
30
|
+
print("Only one root folder found, no selection needed.")
|
|
31
|
+
|
|
32
|
+
else:
|
|
33
|
+
print("Choose Root Folder to restore into, default [0]:")
|
|
34
|
+
|
|
35
|
+
for i in range(len(rootFolder)):
|
|
36
|
+
print("[" + str(i) + "] | Radarr-ID: " + str(rootFolder[i]["id"]) + " | Accessible: " + str(rootFolder[i]["accessible"]) + " | Free Space: " + str(rootFolder[i]["freeSpace"]) + " | Path: " + str(rootFolder[i]["path"]))
|
|
37
|
+
|
|
38
|
+
selectedRootFolderPath = rootFolder[functions.getNumUserInput(len(rootFolder) - 1)]["path"]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
## Load file
|
|
42
|
+
try:
|
|
43
|
+
with open(filename, 'r') as f:
|
|
44
|
+
backupdata = json.load(f)
|
|
45
|
+
print("Loaded contents of:", filename)
|
|
46
|
+
except Exception as e:
|
|
47
|
+
sys.exit("An Error occurred: " + str(e))
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
## Select Qualities
|
|
51
|
+
qualities_set = set()
|
|
52
|
+
quality_dictionary = {}
|
|
53
|
+
|
|
54
|
+
for tmdbID, details in backupdata.items():
|
|
55
|
+
qualities_set.add(details["quality"])
|
|
56
|
+
|
|
57
|
+
for x in qualities_set:
|
|
58
|
+
if x == -1:
|
|
59
|
+
print("There where was no Quality Data found for these movies, which Profile should be used for these?")
|
|
60
|
+
|
|
61
|
+
for i in range(len(qualityProfiles)):
|
|
62
|
+
print("[" + str(i) + "] | ID: " + str(qualityProfiles[i]["id"]) + " | Name: " + str(qualityProfiles[i]["name"]))
|
|
63
|
+
|
|
64
|
+
quality_dictionary[x] = qualityProfiles[functions.getNumUserInput(len(qualityProfiles) - 1)]["id"]
|
|
65
|
+
|
|
66
|
+
print("Profile-Id: " + str(quality_dictionary[x]) + " is now associated with the movies that had no data found.")
|
|
67
|
+
break
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
print("Which Quality Profile should be used for importing ==> " + str(x) + "P <== movies?")
|
|
71
|
+
|
|
72
|
+
for i in range(len(qualityProfiles)):
|
|
73
|
+
print("[" + str(i) + "] | ID: " + str(qualityProfiles[i]["id"]) + " | Name: " + str(qualityProfiles[i]["name"]))
|
|
74
|
+
|
|
75
|
+
quality_dictionary[x] = qualityProfiles[functions.getNumUserInput(len(qualityProfiles) - 1)]["id"]
|
|
76
|
+
|
|
77
|
+
print(str(x) + "p associated with Profile-Id: " + str(quality_dictionary[x]))
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
## Making data for import
|
|
81
|
+
jsonDataList = []
|
|
82
|
+
for movie, details in backupdata.items():
|
|
83
|
+
match app:
|
|
84
|
+
case "radarr":
|
|
85
|
+
id_text = "tmdbId"
|
|
86
|
+
id_cont = details["tmdbId"]
|
|
87
|
+
case "sonarr":
|
|
88
|
+
id_text = "tvdbId"
|
|
89
|
+
id_cont = details["tvdbId"]
|
|
90
|
+
|
|
91
|
+
jsonDataList.append({
|
|
92
|
+
"title": str(details["title"]),
|
|
93
|
+
str(id_text): int(id_cont),
|
|
94
|
+
"qualityProfileId": quality_dictionary[int(details["quality"])],
|
|
95
|
+
"rootFolderPath": str(selectedRootFolderPath),
|
|
96
|
+
"monitored": bool(details["monitored"]),
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
match app:
|
|
100
|
+
case "radarr":
|
|
101
|
+
functions.postJsonData(api.radarr.movieListUrl, api.radarr.apiKey, jsonDataList)
|
|
102
|
+
case "sonarr":
|
|
103
|
+
functions.postJsonData(api.sonarr.seriesListUrl, api.sonarr.apiKey, jsonDataList)
|
|
104
|
+
|
|
105
|
+
print("Successfully imported all entries.\nExiting...")
|
|
106
|
+
sys.exit()
|
|
107
|
+
|
|
108
|
+
if __name__ == "__main__":
|
|
109
|
+
run()
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: backuparr
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A lightweight, efficient way to backup your media library without duplicating large video files.
|
|
5
|
+
Author: Daniel Mayhan
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 DanielMayhan
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Requires-Python: >=3.14
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
License-File: LICENSE
|
|
31
|
+
Requires-Dist: requests
|
|
32
|
+
Requires-Dist: python-dotenv
|
|
33
|
+
Dynamic: license-file
|
|
34
|
+
|
|
35
|
+
# BackupArr
|
|
36
|
+
|
|
37
|
+
> [!NOTE]
|
|
38
|
+
> **AI-Generated Documentation:** This README was drafted with the assistance of AI, but none of the Code in this Project was written by AI.
|
|
39
|
+
|
|
40
|
+
Welcome to the **BackupArr** repository. This project is currently in its early development stages (**Pre-Alpha**) and aims to provide a lightweight, efficient way to "back up" your media library without duplicating large video files.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## The Concept
|
|
45
|
+
Traditional backups for media libraries are often prohibitively expensive due to massive file sizes. **BackupArr** changes the paradigm: instead of backing up the media files themselves, this tool maps your library metadata back to its source. In the event of data loss, you can restore the library state to your download client to re-acquire the media from the swarm.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Project Status: Alpha
|
|
50
|
+
**Warning:** This software is experimental and **Work in Progress**. It is not recommended for production use. Any and all code are subject to radical changes as development progresses.
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Getting Started (Development)
|
|
55
|
+
|
|
56
|
+
1. **Clone the repository:**
|
|
57
|
+
```bash
|
|
58
|
+
git clone https://github.com/DanielMayhan/BackupArr.git
|
|
59
|
+
cd BackupArr
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
2. **Configure Environment:**
|
|
63
|
+
Copy the sample configuration and add your Radarr API key & Url.
|
|
64
|
+
```bash
|
|
65
|
+
cp .env.example .env
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Usage
|
|
71
|
+
* **Command Structure:**
|
|
72
|
+
```bash
|
|
73
|
+
python3 main.py [backup|restore] [radarr|sonarr] [filename].json
|
|
74
|
+
* **Example Command:**
|
|
75
|
+
```bash
|
|
76
|
+
python3 main.py backup radarr radarr_backup.json
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Roadmap
|
|
82
|
+
These are features and code changes that are either currently being worked on or are planned to be implemented later!
|
|
83
|
+
* Code Cleanup
|
|
84
|
+
* Unification of all text in-/outputs
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Contributing
|
|
90
|
+
We welcome all input during these early stages!
|
|
91
|
+
* Open an **Issue** to discuss new ideas or report bugs.
|
|
92
|
+
* Submit a **Pull Request** to help with early-stage logic.
|
|
93
|
+
* Check the **Discussions** tab to help define the project roadmap.
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## License
|
|
98
|
+
This project is licensed under the [MIT License](LICENSE).
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
backuparr/api_stuff.py
|
|
5
|
+
backuparr/backup.py
|
|
6
|
+
backuparr/functions.py
|
|
7
|
+
backuparr/main.py
|
|
8
|
+
backuparr/restore.py
|
|
9
|
+
backuparr.egg-info/PKG-INFO
|
|
10
|
+
backuparr.egg-info/SOURCES.txt
|
|
11
|
+
backuparr.egg-info/dependency_links.txt
|
|
12
|
+
backuparr.egg-info/entry_points.txt
|
|
13
|
+
backuparr.egg-info/requires.txt
|
|
14
|
+
backuparr.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
backuparr
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "backuparr"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "A lightweight, efficient way to backup your media library without duplicating large video files."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.14"
|
|
11
|
+
license = {file = "LICENSE"}
|
|
12
|
+
authors = [{name = "Daniel Mayhan"}]
|
|
13
|
+
dependencies = ["requests", "python-dotenv"]
|
|
14
|
+
|
|
15
|
+
[project.scripts]
|
|
16
|
+
backuparr = "backuparr.main:run"
|