Skip to content

Commit 7cc7292

Browse files
committed
Very basic GUI is done
1 parent df92ae9 commit 7cc7292

File tree

228 files changed

+376450
-8
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

228 files changed

+376450
-8
lines changed

.bowerrc

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"directory": "bower_components",
3+
"json": "bower.json"
4+
}

bower.json

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
{
2+
"name": "NZB Hydra",
3+
"version": "0.0.1",
4+
"authors": [
5+
"Theotherp"
6+
],
7+
"license": "Apache-2.0",
8+
"private": true,
9+
"ignore": [
10+
"**/.*",
11+
"node_modules",
12+
"static/lib",
13+
"test",
14+
"tests",
15+
"cache",
16+
"bower_components"
17+
],
18+
"dependencies": {
19+
"angular-animate": "~1.3.11",
20+
"angular-bootstrap": "~0.12.0",
21+
"angular-filter": "~0.5.2",
22+
"angular-cookie": "~4.0.6",
23+
"angular-growl-2": "~0.7.3",
24+
"angular-route": "~1.3.11",
25+
"angular-loading-bar": "~0.6.0",
26+
"animate-css": "~3.2.0",
27+
"filesize": "~3.0.2",
28+
"bootstrap": "~3.3.2",
29+
"superagent": "~0.21.0",
30+
"momentjs": "~2.9.0",
31+
"typicons": "~2.0.7",
32+
"underscore": "~1.7.0"
33+
},
34+
"description": "",
35+
"main": "nzbhydra.py",
36+
"moduleType": [
37+
"globals"
38+
],
39+
"homepage": "https://github.com/theotherp/nzbhydra"
40+
}

gruntfile.js

+7-6
Original file line numberDiff line numberDiff line change
@@ -97,21 +97,21 @@ module.exports = function (grunt) {
9797
},
9898

9999
less: {
100-
crapture: {
100+
nzbhydra: {
101101
options: {
102102
strictMath: true,
103103
paths: ["nzbhydra/static/less"]
104104
},
105105
files: {
106-
'nzbhydra/static/css/crapture.css': 'nzbhydra/static/less/nzbhydra.less'
106+
'nzbhydra/static/css/nzbhydra.css': 'nzbhydra/static/less/nzbhydra.less'
107107
}
108108
},
109109
bootstrap: {
110110
options: {
111111
strictMath: true
112112
},
113113
files: {
114-
'nzbhydra/static/css/bootstrap.css': 'nzbhydra/static/less/bootstrap/nzbhydra.less'
114+
'nzbhydra/static/css/bootstrap.css': 'nzbhydra/static/less/bootstrap/bootstrap.less'
115115
}
116116
}
117117
},
@@ -162,13 +162,13 @@ module.exports = function (grunt) {
162162

163163
watch: {
164164
mystuff: {
165-
files: ['nzbhydra/static/**/*.*', '!nzbhydra/static/less/**/*.less'], tasks: ['shell'], options: {
165+
files: ['nzbhydra/static/**/*.*', '!nzbhydra/static/less/**/*.less'], options: {
166166
livereload: true
167167
}
168168
},
169169

170170
crapLess: {
171-
files: ['nzbhydra/static/less/nzbhydra.less'], tasks: ['less:nzbhydra', 'shell']
171+
files: ['nzbhydra/static/less/nzbhydra.less', 'nzbhydra/static/less/bootstrap/variables.less'], tasks: ['less:nzbhydra']
172172
},
173173

174174
bootstrapLess: {
@@ -199,7 +199,6 @@ module.exports = function (grunt) {
199199
grunt.loadNpmTasks('grunt-contrib-watch');
200200
grunt.loadNpmTasks('grunt-contrib-copy');
201201
grunt.loadNpmTasks('grunt-contrib-less');
202-
grunt.loadNpmTasks('grunt-shell');
203202

204203

205204
grunt.registerTask(
@@ -222,4 +221,6 @@ module.exports = function (grunt) {
222221

223222
grunt.registerTask('default', ['watch']);
224223

224+
grunt.registerTask('copy', ['bowercopy']);
225+
225226
};

nzbhydra/api.py

+121
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
from itertools import groupby
2+
3+
from marshmallow import Schema, fields
4+
5+
import config
6+
from config import init
7+
8+
init("ResultProcessing.duplicateSizeThresholdInPercent", 0.1, float)
9+
init("ResultProcessing.duplicateAgeThreshold", 36000, int)
10+
11+
categories = {'Console': {'code': [1000, 1010, 1020, 1030, 1040, 1050, 1060, 1070, 1080], 'pretty': 'Console'},
12+
'Movie': {'code': [2000, 2010, 2020], 'pretty': 'Movie'},
13+
'Movie_HD': {'code': [2040, 2050, 2060], 'pretty': 'HD'},
14+
'Movie_SD': {'code': [2030], 'pretty': 'SD'},
15+
'Audio': {'code': [3000, 3010, 3020, 3030, 3040], 'pretty': 'Audio'},
16+
'PC': {'code': [4000, 4010, 4020, 4030, 4040, 4050, 4060, 4070], 'pretty': 'PC'},
17+
'TV': {'code': [5000, 5020], 'pretty': 'TV'},
18+
'TV_SD': {'code': [5030], 'pretty': 'SD'},
19+
'TV_HD': {'code': [5040], 'pretty': 'HD'},
20+
'XXX': {'code': [6000, 6010, 6020, 6030, 6040, 6050], 'pretty': 'XXX'},
21+
'Other': {'code': [7000, 7010], 'pretty': 'Other'},
22+
'Ebook': {'code': [7020], 'pretty': 'Ebook'},
23+
'Comics': {'code': [7030], 'pretty': 'Comics'},
24+
}
25+
26+
27+
def find_duplicates(results):
28+
"""
29+
30+
:type results: list[NzbSearchResult]
31+
"""
32+
# TODO we might want to be able to specify more precisely what item we pick of a group of duplicates, for example by indexer priority
33+
34+
# Sort and group by title. We only need to check the items in each individual group against each other because we only consider items with the same title as possible duplicates
35+
sorted_results = sorted(results, key=lambda x: x.title.lower())
36+
grouped_by_title = groupby(sorted_results, key=lambda x: x.title.lower())
37+
grouped_by_sameness = []
38+
39+
for key, group in grouped_by_title:
40+
seen2 = set()
41+
group = list(group)
42+
for i in range(len(group)):
43+
if group[i] in seen2:
44+
continue
45+
seen2.add(group[i])
46+
same_results = [group[i]] # All elements in this list are duplicates of each other
47+
for j in range(i + 1, len(group)):
48+
if group[j] in seen2:
49+
continue
50+
seen2.add(group[j])
51+
if test_for_duplicate(group[i], group[j]):
52+
same_results.append(group[j])
53+
grouped_by_sameness.append(same_results)
54+
55+
return grouped_by_sameness
56+
57+
58+
def test_for_duplicate(search_result_1, search_result_2):
59+
"""
60+
61+
:type search_result_1: NzbSearchResult
62+
:type search_result_2: NzbSearchResult
63+
"""
64+
65+
if search_result_1.title.lower() != search_result_2.title.lower():
66+
return False
67+
size_threshold = config.cfg["ResultProcessing.duplicateSizeThresholdInPercent"]
68+
size_difference = search_result_1.size - search_result_2.size
69+
size_average = (search_result_1.size + search_result_2.size) / 2
70+
size_difference_percent = abs(size_difference / size_average) * 100
71+
72+
73+
# TODO: Ignore age threshold if no precise date is known or account for score (if we have sth like that...)
74+
age_threshold = config.cfg["ResultProcessing.duplicateAgeThreshold"]
75+
same_size = size_difference_percent <= size_threshold
76+
same_age = abs(search_result_1.epoch - search_result_2.epoch) / (1000 * 60) <= age_threshold # epoch difference (ms) to minutes
77+
78+
# If all nweznab providers would provide poster/group in their infos then this would be a lot easier and more precise
79+
# We could also use something to combine several values to a score, say that if a two posts have the exact size their age may differe more or combine relative and absolute size comparison
80+
if same_size and same_age:
81+
return True
82+
83+
84+
class NzbSearchResultSchema(Schema):
85+
title = fields.String()
86+
link = fields.String()
87+
pubDate = fields.String()
88+
epoch = fields.Integer()
89+
pubdate_utc = fields.String()
90+
age_days = fields.Integer()
91+
age_precise = fields.Boolean()
92+
provider = fields.String()
93+
guid = fields.String()
94+
size = fields.Integer()
95+
categories = fields.String() # wthy the fuc doesnt this work with fields.Integer(many=True)
96+
97+
98+
def process_for_internal_api(results):
99+
# do what ever we need do prepare the results to be shown on our own page instead of being returned as newznab-compatible API
100+
"""
101+
102+
:type results: list[NzbSearchResult]
103+
"""
104+
grouped_by_sameness = find_duplicates(results)
105+
106+
107+
# Will be sorted by GUI later anyway but makes debugging easier
108+
results = sorted(grouped_by_sameness, key=lambda x: x[0].epoch, reverse=True)
109+
serialized = []
110+
for g in results:
111+
serialized.append(serialize_nzb_search_result(g).data)
112+
113+
#We give each group of results a unique count value by which they can be identified later even if they're "taken apart"
114+
for count, group in enumerate(serialized):
115+
for i in group:
116+
i["count"] = count
117+
return {"results": serialized}
118+
119+
120+
def serialize_nzb_search_result(result):
121+
return NzbSearchResultSchema(many=True).dump(result)

nzbhydra/config.py

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import profig
2+
3+
cfg = profig.Config()
4+
5+
6+
def load(filename):
7+
global cfg
8+
cfg.read(filename)
9+
#Manually set the source to this settings file so that when syncing the settings are written back. If we don't do this it loads the settings but doesn't write them back. Or we would need to store the
10+
#settings filename and call write(filename)
11+
cfg.sources = [filename]
12+
13+
14+
def init(path, value, type):
15+
global cfg
16+
cfg.init(path, value, type)
17+
#Write this to console for now, later we can use it to collect all available/expected config data
18+
print("Initializing configuration with path %s and value %s" % (path, value))
19+
20+

nzbhydra/database.py

+93
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import json
2+
import datetime
3+
import arrow
4+
from dateutil.tz import tzutc
5+
6+
from peewee import *
7+
8+
db = SqliteDatabase(None)
9+
10+
11+
class JSONField(TextField):
12+
db_field = "text"
13+
14+
def db_value(self, value):
15+
return json.dumps(value)
16+
17+
def python_value(self, value):
18+
return json.loads(value)
19+
20+
21+
class DateTimeUTCField(DateTimeField):
22+
db_field = "datetime"
23+
24+
def db_value(self, value):
25+
return arrow.get(value).datetime if value is not None else None
26+
27+
def python_value(self, value):
28+
return arrow.get(value, tzinfo=tzutc())
29+
30+
31+
class Provider(Model):
32+
name = CharField(unique=True)
33+
module = CharField()
34+
enabled = BooleanField(default=True)
35+
settings = JSONField(default={})
36+
37+
class Meta:
38+
database = db
39+
40+
41+
class ProviderSearch(Model):
42+
provider = ForeignKeyField(Provider)
43+
time = DateTimeField(default=datetime.datetime.utcnow())
44+
45+
query = CharField(null=True)
46+
query_generated = BooleanField(default=False)
47+
identifier_key = CharField(null=True)
48+
identifier_value = CharField(null=True)
49+
categories = JSONField(default=[])
50+
season = IntegerField(null=True)
51+
episode = IntegerField(null=True)
52+
53+
successful = BooleanField(default=False)
54+
results = IntegerField(null=True) # number of results returned
55+
56+
class Meta:
57+
database = db # This model uses the "people.db" database.
58+
59+
60+
class ProviderApiAccess(Model):
61+
provider = ForeignKeyField(Provider)
62+
time = DateTimeUTCField(default=datetime.datetime.utcnow())
63+
type = CharField() # search, download, comments, nfo?
64+
response_successful = BooleanField(default=False)
65+
response_time = IntegerField(null=True)
66+
error = CharField(null=True)
67+
68+
class Meta:
69+
database = db
70+
71+
72+
class ProviderSearchApiAccess(Model):
73+
search = ForeignKeyField(ProviderSearch, related_name="api_accesses")
74+
api_access = ForeignKeyField(ProviderApiAccess, related_name="api_accesses")
75+
76+
class Meta:
77+
database = db
78+
79+
80+
class ProviderStatus(Model):
81+
provider = ForeignKeyField(Provider, related_name="status")
82+
first_failure = DateTimeUTCField(default=datetime.datetime.utcnow(), null=True)
83+
latest_failure = DateTimeUTCField(default=datetime.datetime.utcnow(), null=True)
84+
disabled_until = DateTimeUTCField(default=datetime.datetime.utcnow(), null=True)
85+
level = IntegerField(default=0)
86+
reason = CharField(null=True)
87+
disabled_permanently = BooleanField(default=False) #Set to true if an error occurred that probably won't fix itself (e.g. when the apikey is wrong)
88+
89+
def __repr__(self):
90+
return "%s in status %d. First failure: %s. Latest Failure: %s. Reason: %s. Disabled until: %s" % (self.provider, self.level, self.first_failure, self.latest_failure, self.reason, self.disabled_until)
91+
92+
class Meta:
93+
database = db

nzbhydra/datestuff.py

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#can be mocked
2+
import datetime
3+
4+
5+
def now():
6+
return datetime.datetime.now()

0 commit comments

Comments
 (0)