mirror of
https://github.com/hrfee/jellyfin-accounts.git
synced 2025-12-19 16:51:13 +00:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 27169e4e0d | |||
| db3b992857 | |||
| 89c132e92e | |||
| 7bda2f4141 | |||
| 71f05f2348 | |||
| 94e69ad090 | |||
| a3d3d97b3b | |||
| 781306f1ef | |||
| a62eab9565 | |||
| a2a2abc7f2 | |||
| fa0527c6a7 | |||
| b33922059c |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -12,8 +12,10 @@ jfa/
|
||||
colors.txt
|
||||
theme.css
|
||||
jellyfin_accounts/__pycache__/
|
||||
jellyfin_accounts/data/static/*.css
|
||||
old/
|
||||
.jf-accounts/
|
||||
requirements.txt
|
||||
package-lock.json
|
||||
video/
|
||||
scss/bs5/*.css*
|
||||
scss/bs4/*.css*
|
||||
|
||||
@@ -9,13 +9,13 @@ server = http://jellyfin.local:8096
|
||||
public_server = https://jellyf.in:443
|
||||
; this and below settings will show on the jellyfin dashboard when the program connects. you may as well leave them alone.
|
||||
client = jf-accounts
|
||||
version = 0.3.0
|
||||
version = 0.3.2
|
||||
device = jf-accounts
|
||||
device_id = jf-accounts-0.3.0
|
||||
device_id = jf-accounts-0.3.2
|
||||
|
||||
[ui]
|
||||
; settings related to the ui and program functionality.
|
||||
; choose the look of jellyfin-accounts.
|
||||
; default appearance for all users.
|
||||
theme = Jellyfin (Dark)
|
||||
; set 0.0.0.0 to run on localhost
|
||||
host = 0.0.0.0
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
__version__ = "0.3.0"
|
||||
__version__ = "0.3.6"
|
||||
|
||||
import secrets
|
||||
import configparser
|
||||
@@ -218,7 +218,7 @@ elif "Custom" in current_theme and "custom_css" in config["files"]:
|
||||
try:
|
||||
css_path = Path(config["files"]["custom_css"])
|
||||
shutil.copy(css_path, (local_dir / "static" / css_path.name))
|
||||
log.debug('Loaded custom CSS "{css_path.name}"')
|
||||
log.debug(f'Loaded custom CSS "{css_path.name}"')
|
||||
css_file = css_path.name
|
||||
except FileNotFoundError:
|
||||
log.error(
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
"description": "Settings related to the UI and program functionality."
|
||||
},
|
||||
"theme": {
|
||||
"name": "Look",
|
||||
"name": "Default Look",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"type": "select",
|
||||
@@ -81,7 +81,7 @@
|
||||
"Custom CSS"
|
||||
],
|
||||
"value": "Jellyfin (Dark)",
|
||||
"description": "Choose the look of jellyfin-accounts."
|
||||
"description": "Default appearance for all users."
|
||||
},
|
||||
"host": {
|
||||
"name": "Address",
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -16,6 +16,7 @@ function serializeForm(id) {
|
||||
case 'password':
|
||||
case 'select-one':
|
||||
case 'email':
|
||||
case 'number':
|
||||
formData[name] = el.value;
|
||||
break;
|
||||
};
|
||||
|
||||
@@ -13,6 +13,7 @@ for (var i = 0; i < authRadios.length; i++) {
|
||||
checkAuthRadio();
|
||||
});
|
||||
};
|
||||
|
||||
function checkEmailRadio() {
|
||||
document.getElementById('emailNextButton').href = '#page-5';
|
||||
document.getElementById('valBackButton').href = '#page-7';
|
||||
@@ -35,6 +36,7 @@ for (var i = 0; i < emailRadios.length; i++) {
|
||||
checkEmailRadio();
|
||||
});
|
||||
};
|
||||
|
||||
function checkSSL() {
|
||||
var label = document.getElementById('emailSSL_TLSLabel');
|
||||
if (document.getElementById('emailSSL_TLS').checked) {
|
||||
@@ -101,16 +103,15 @@ document.getElementById('jfTestButton').onclick = function() {
|
||||
jfData['jfHost'] = document.getElementById('jfHost').value;
|
||||
jfData['jfUser'] = document.getElementById('jfUser').value;
|
||||
jfData['jfPassword'] = document.getElementById('jfPassword').value;
|
||||
$.ajax('/testJF', {
|
||||
type : 'POST',
|
||||
dataType : 'json',
|
||||
contentType : 'application/json',
|
||||
data : JSON.stringify(jfData),
|
||||
complete: function(response) {
|
||||
var req = new XMLHttpRequest();
|
||||
req.open("POST", "/testJF", true);
|
||||
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
|
||||
req.responseType = 'json';
|
||||
req.onreadystatechange = function() {
|
||||
if (this.readyState == 4) {
|
||||
testButton.disabled = false;
|
||||
testButton.className = '';
|
||||
var success = response['responseJSON']['success'];
|
||||
if (success == true) {
|
||||
if (this.response['success'] == true) {
|
||||
testButton.classList.add('btn', 'btn-success');
|
||||
testButton.textContent = 'Success';
|
||||
nextButton.classList.remove('disabled');
|
||||
@@ -118,9 +119,10 @@ document.getElementById('jfTestButton').onclick = function() {
|
||||
} else {
|
||||
testButton.classList.add('btn', 'btn-danger');
|
||||
testButton.textContent = 'Failed';
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
};
|
||||
};
|
||||
req.send(JSON.stringify(jfData));
|
||||
};
|
||||
|
||||
document.getElementById('submitButton').onclick = function() {
|
||||
@@ -158,7 +160,7 @@ document.getElementById('submitButton').onclick = function() {
|
||||
if (document.getElementById('emailDisabledRadio').checked) {
|
||||
config['password_resets']['enabled'] = 'false';
|
||||
config['invite_emails']['enabled'] = 'false';
|
||||
} else {
|
||||
} else {
|
||||
if (document.getElementById('emailSMTPRadio').checked) {
|
||||
if (document.getElementById('emailSSL_TLS').checked) {
|
||||
config['smtp']['encryption'] = 'ssl_tls';
|
||||
@@ -211,24 +213,24 @@ document.getElementById('submitButton').onclick = function() {
|
||||
config['password_validation']['number'] = document.getElementById('valNumber').value;
|
||||
config['password_validation']['special'] = document.getElementById('valSpecial').value;
|
||||
} else {
|
||||
config['password_validation']['enabled'] = 'false';
|
||||
config['password_validation']['enabled'] = 'false';
|
||||
};
|
||||
// Page 9: Messages
|
||||
config['ui']['contact_message'] = document.getElementById('msgContact').value;
|
||||
config['ui']['help_message'] = document.getElementById('msgHelp').value;
|
||||
config['ui']['success_message'] = document.getElementById('msgSuccess').value;
|
||||
console.log(config);
|
||||
$.ajax('/modifyConfig', {
|
||||
type : 'POST',
|
||||
dataType : 'json',
|
||||
contentType : 'application/json',
|
||||
data : JSON.stringify(config),
|
||||
complete: function(response) {
|
||||
// Send it
|
||||
var req = new XMLHttpRequest();
|
||||
req.open("POST", "/modifyConfig", true);
|
||||
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
|
||||
req.responseType = 'json';
|
||||
req.onreadystatechange = function() {
|
||||
if (this.readyState == 4) {
|
||||
submitButton.disabled = false;
|
||||
submitButton.className = '';
|
||||
submitButton.classList.add('btn', 'btn-success');
|
||||
submitButton.textContent = 'Success';
|
||||
}
|
||||
});
|
||||
};
|
||||
};
|
||||
req.send(JSON.stringify(config));
|
||||
};
|
||||
|
||||
|
||||
@@ -14,7 +14,42 @@
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
<!-- Bootstrap CSS -->
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="{{ css_file }}">
|
||||
<script>
|
||||
function getCookie(cname) {
|
||||
var name = cname + "=";
|
||||
var decodedCookie = decodeURIComponent(document.cookie);
|
||||
var ca = decodedCookie.split(';');
|
||||
for(var i = 0; i < ca.length; i++) {
|
||||
var c = ca[i];
|
||||
while (c.charAt(0) == ' ') {
|
||||
c = c.substring(1);
|
||||
}
|
||||
if (c.indexOf(name) == 0) {
|
||||
return c.substring(name.length, c.length);
|
||||
}
|
||||
}
|
||||
return "";
|
||||
};
|
||||
{% if bs5 %}
|
||||
const bsVersion = 5;
|
||||
{% else %}
|
||||
const bsVersion = 4;
|
||||
{% endif %}
|
||||
console.log('create');
|
||||
var css = document.createElement('link');
|
||||
css.setAttribute('rel', 'stylesheet');
|
||||
css.setAttribute('type', 'text/css');
|
||||
var cssCookie = getCookie("css");
|
||||
if (cssCookie.includes('bs' + bsVersion)) {
|
||||
console.log('href');
|
||||
css.setAttribute('href', cssCookie);
|
||||
} else {
|
||||
console.log('href');
|
||||
css.setAttribute('href', '{{ css_file }}');
|
||||
};
|
||||
console.log('append');
|
||||
document.head.appendChild(css);
|
||||
</script>
|
||||
{% if not bs5 %}
|
||||
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script>
|
||||
{% endif %}
|
||||
@@ -53,10 +88,42 @@
|
||||
margin-top: 5%;
|
||||
color: grey;
|
||||
}
|
||||
.circle {
|
||||
/*margin-left: 1rem;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
border-radius: 50%;
|
||||
z-index: 5000;*/
|
||||
-webkit-transition: all 300ms cubic-bezier(0.550, 0.055, 0.675, 0.190);
|
||||
-moz-transition: all 300ms cubic-bezier(0.550, 0.055, 0.675, 0.190);
|
||||
-o-transition: all 300ms cubic-bezier(0.550, 0.055, 0.675, 0.190);
|
||||
transition: all 300ms cubic-bezier(0.550, 0.055, 0.675, 0.190); /* easeInCubic */
|
||||
}
|
||||
.smooth-transition {
|
||||
-webkit-transition: all 300ms cubic-bezier(0.550, 0.055, 0.675, 0.190);
|
||||
-moz-transition: all 300ms cubic-bezier(0.550, 0.055, 0.675, 0.190);
|
||||
-o-transition: all 300ms cubic-bezier(0.550, 0.055, 0.675, 0.190);
|
||||
transition: all 300ms cubic-bezier(0.550, 0.055, 0.675, 0.190); /* easeincubic */
|
||||
}
|
||||
.rotated {
|
||||
transform: rotate(180deg);
|
||||
-webkit-transition: all 150ms cubic-bezier(0.770, 0.000, 0.175, 1.000);
|
||||
-moz-transition: all 150ms cubic-bezier(0.770, 0.000, 0.175, 1.000);
|
||||
-o-transition: all 150ms cubic-bezier(0.770, 0.000, 0.175, 1.000);
|
||||
transition: all 150ms cubic-bezier(0.770, 0.000, 0.175, 1.000); /* easeInOutQuart */
|
||||
}
|
||||
|
||||
.not-rotated {
|
||||
transform: rotate(0deg);
|
||||
-webkit-transition: all 150ms cubic-bezier(0.770, 0.000, 0.175, 1.000);
|
||||
-moz-transition: all 150ms cubic-bezier(0.770, 0.000, 0.175, 1.000);
|
||||
-o-transition: all 150ms cubic-bezier(0.770, 0.000, 0.175, 1.000);
|
||||
transition: all 150ms cubic-bezier(0.770, 0.000, 0.175, 1.000); /* easeInOutQuart */
|
||||
}
|
||||
</style>
|
||||
<title>Admin</title>
|
||||
</head>
|
||||
<body>
|
||||
<body class="smooth-transition">
|
||||
<div class="modal fade" id="login" role="dialog" aria-labelledby="login" aria-hidden="true" data-backdrop="static">
|
||||
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||
<div class="modal-content">
|
||||
@@ -171,9 +238,11 @@
|
||||
<h1>
|
||||
Accounts admin
|
||||
</h1>
|
||||
<button type="button" class="btn btn-secondary" id="openSettings">
|
||||
Settings <i class="fa fa-cog"></i>
|
||||
</button>
|
||||
<div class="btn-group" role="group" id="headerButtons">
|
||||
<button type="button" class="btn btn-primary" id="openSettings">
|
||||
Settings <i class="fa fa-cog"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="card mb-3 linkGroup">
|
||||
<div class="card-header">Current Invites</div>
|
||||
<ul class="list-group list-group-flush" id="invites">
|
||||
@@ -183,29 +252,66 @@
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">Generate Invite</div>
|
||||
<div class="card-body">
|
||||
<form action="#" method="POST" id="inviteForm">
|
||||
<div class="form-group">
|
||||
<label for="hours">Hours</label>
|
||||
<select class="form-control" id="hours" name="hours">
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="minutes">Minutes</label>
|
||||
<select class="form-control" id="minutes" name="minutes">
|
||||
</select>
|
||||
</div>
|
||||
{% if email_enabled %}
|
||||
<div class="form-group">
|
||||
<label for="send_to_address">Send invite to address</label>
|
||||
<div class="input-group">
|
||||
<div class="input-group-text">
|
||||
<input class="form-check-input" type="checkbox" onchange="document.getElementById('send_to_address').disabled = !this.checked;" aria-label="Checkbox to allow input of email address" id="send_to_address_enabled">
|
||||
</div>
|
||||
<input type="email" class="form-control" placeholder="example@example.com" id="send_to_address" disabled>
|
||||
<form action="#" method="POST" id="inviteForm" class="container">
|
||||
<div class="row align-items-start">
|
||||
<div class="col">
|
||||
<div class="form-group">
|
||||
<label for="days">Days</label>
|
||||
<select class="form-control form-select" id="days" name="days">
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="hours">Hours</label>
|
||||
<select class="form-control form-select" id="hours" name="hours">
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="minutes">Minutes</label>
|
||||
<select class="form-control form-select" id="minutes" name="minutes">
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<button type="submit" id="generateSubmit" class="btn btn-primary" style="margin-top: 1rem;">Generate</button>
|
||||
<div class="col">
|
||||
<div class="form-group">
|
||||
<label for="multiUseCount">
|
||||
Multiple uses
|
||||
</label>
|
||||
<div class="input-group">
|
||||
<div class="input-group-text">
|
||||
<input class="form-check-input" type="checkbox" onchange="document.getElementById('multiUseCount').disabled = !this.checked; document.getElementById('noUseLimit').disabled = !this.checked" aria-label="Checkbox to allow choice of invite usage limit" name="multiple-uses" id="multiUseEnabled">
|
||||
</div>
|
||||
<input type="number" class="form-control" name="remaining-uses" id="multiUseCount">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group form-check" style="margin-top: 1rem; margin-bottom: 1rem;">
|
||||
<input class="form-check-input" type="checkbox" value="" name="no-limit" id="noUseLimit" onchange="document.getElementById('multiUseCount').disabled = this.checked; if (this.checked) { document.getElementById('noLimitWarning').style = 'display: block;' } else { document.getElementById('noLimitWarning').style = 'display: none;'; }">
|
||||
<label class="form-check-label" for="noUseLimit">
|
||||
No use limit
|
||||
</label>
|
||||
<div id="noLimitWarning" class="form-text" style="display: none;">Warning: Unlimited usage invites pose a risk if published online.</div>
|
||||
</div>
|
||||
{% if email_enabled %}
|
||||
<div class="form-group">
|
||||
<label for="send_to_address">Send invite to address</label>
|
||||
<div class="input-group">
|
||||
<div class="input-group-text">
|
||||
<input class="form-check-input" type="checkbox" onchange="document.getElementById('send_to_address').disabled = !this.checked;" aria-label="Checkbox to allow input of email address" id="send_to_address_enabled">
|
||||
</div>
|
||||
<input type="email" class="form-control" placeholder="example@example.com" id="send_to_address" disabled>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="form-group d-flex float-right">
|
||||
<button type="submit" id="generateSubmit" class="btn btn-primary" style="margin-top: 1rem;">
|
||||
Generate
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -156,6 +156,9 @@
|
||||
submitButton.replaceChild(newSpan, oldSpan);
|
||||
};
|
||||
document.getElementById('accountForm').onsubmit = function() {
|
||||
if (document.getElementById('errorMessage')) {
|
||||
document.getElementById('errorMessage').remove();
|
||||
}
|
||||
toggleSpinner();
|
||||
var send = serializeForm('accountForm');
|
||||
send['code'] = code;
|
||||
@@ -171,12 +174,18 @@
|
||||
if (this.readyState == 4) {
|
||||
toggleSpinner();
|
||||
var data = this.response;
|
||||
if ('error' in data) {
|
||||
var text = document.createTextNode(data['error']);
|
||||
if ('error' in data || data['success'] == false) {
|
||||
if (typeof(data['error']) != 'undefined') {
|
||||
var errorMessage = data['error'];
|
||||
} else {
|
||||
var errorMessage = 'Unknown Error';
|
||||
}
|
||||
var text = document.createTextNode(errorMessage);
|
||||
var error = document.createElement('button');
|
||||
error.classList.add('btn', 'btn-outline-danger');
|
||||
error.setAttribute('disabled', '');
|
||||
error.appendChild(text);
|
||||
error.id = 'errorMessage';
|
||||
document.getElementById('errorBox').appendChild(error);
|
||||
} else {
|
||||
var valid = true
|
||||
|
||||
@@ -190,7 +190,7 @@ class Jellyfin:
|
||||
)
|
||||
|
||||
def newUser(self, username: str, password: str):
|
||||
for user in self.getUsers():
|
||||
for user in self.getUsers(public=False):
|
||||
if user["Name"] == username:
|
||||
raise self.UserExistsError
|
||||
response = requests.post(
|
||||
|
||||
@@ -6,6 +6,12 @@ from jellyfin_accounts import web_log as log
|
||||
from jellyfin_accounts.web_api import config, checkInvite, validator
|
||||
|
||||
|
||||
if config.getboolean("ui", "bs5"):
|
||||
bsVersion = 5
|
||||
else:
|
||||
bsVersion = 4
|
||||
|
||||
|
||||
@app.errorhandler(404)
|
||||
def page_not_found(e):
|
||||
return (
|
||||
@@ -35,11 +41,11 @@ def admin():
|
||||
def static_proxy(path):
|
||||
if "html" not in path:
|
||||
if "admin.js" in path:
|
||||
if config.getboolean("ui", "bs5"):
|
||||
bsVersion = 5
|
||||
else:
|
||||
bsVersion = 4
|
||||
return render_template("admin.js", bsVersion=bsVersion)
|
||||
return (
|
||||
render_template("admin.js", bsVersion=bsVersion, css_file=css_file),
|
||||
200,
|
||||
{"Content-Type": "text/javascript"},
|
||||
)
|
||||
return app.send_static_file(path)
|
||||
return (
|
||||
render_template(
|
||||
|
||||
@@ -20,21 +20,49 @@ from jellyfin_accounts import web_log as log
|
||||
from jellyfin_accounts.validate_password import PasswordValidator
|
||||
|
||||
|
||||
def checkInvite(code, delete=False):
|
||||
def format_datetime(dt):
|
||||
result = dt.strftime(config["email"]["date_format"])
|
||||
if config.getboolean("email", "use_24h"):
|
||||
result += f' {dt.strftime("%H:%M")}'
|
||||
else:
|
||||
result += f' {dt.strftime("%I:%M %p")}'
|
||||
return result
|
||||
|
||||
|
||||
def checkInvite(code, used=False, username=None):
|
||||
current_time = datetime.datetime.now()
|
||||
invites = dict(data_store.invites)
|
||||
match = False
|
||||
for invite in invites:
|
||||
if (
|
||||
"remaining-uses" not in invites[invite]
|
||||
and "no-limit" not in invites[invite]
|
||||
):
|
||||
invites[invite]["remaining-uses"] = 1
|
||||
expiry = datetime.datetime.strptime(
|
||||
invites[invite]["valid_till"], "%Y-%m-%dT%H:%M:%S.%f"
|
||||
)
|
||||
if current_time >= expiry:
|
||||
log.debug(f"Housekeeping: Deleting old invite {invite}")
|
||||
if current_time >= expiry or (
|
||||
"no-limit" not in invites[invite] and invites[invite]["remaining-uses"] < 1
|
||||
):
|
||||
log.debug(f"Housekeeping: Deleting expired invite {invite}")
|
||||
del data_store.invites[invite]
|
||||
elif invite == code:
|
||||
match = True
|
||||
if delete:
|
||||
del data_store.invites[code]
|
||||
if used:
|
||||
delete = False
|
||||
inv = dict(data_store.invites[code])
|
||||
if "used-by" not in inv:
|
||||
inv["used-by"] = []
|
||||
if "remaining-uses" in inv:
|
||||
if inv["remaining-uses"] == 1:
|
||||
delete = True
|
||||
del data_store.invites[code]
|
||||
elif "no-limit" not in invites[invite]:
|
||||
inv["remaining-uses"] -= 1
|
||||
inv["used-by"].append([username, format_datetime(current_time)])
|
||||
if not delete:
|
||||
data_store.invites[code] = inv
|
||||
return match
|
||||
|
||||
|
||||
@@ -157,7 +185,7 @@ def newUser():
|
||||
return jsonify({"error": error})
|
||||
except:
|
||||
return jsonify({"error": "Unknown error"})
|
||||
checkInvite(data["code"], delete=True)
|
||||
checkInvite(data["code"], used=True, username=data["username"])
|
||||
if user.status_code == 200:
|
||||
try:
|
||||
policy = data_store.user_template
|
||||
@@ -200,9 +228,19 @@ def newUser():
|
||||
def generateInvite():
|
||||
current_time = datetime.datetime.now()
|
||||
data = request.get_json()
|
||||
delta = datetime.timedelta(hours=int(data["hours"]), minutes=int(data["minutes"]))
|
||||
delta = datetime.timedelta(
|
||||
days=int(data["days"]), hours=int(data["hours"]), minutes=int(data["minutes"])
|
||||
)
|
||||
invite_code = secrets.token_urlsafe(16)
|
||||
invite = {}
|
||||
invite["created"] = format_datetime(current_time)
|
||||
if data["multiple-uses"]:
|
||||
if data["no-limit"]:
|
||||
invite["no-limit"] = True
|
||||
else:
|
||||
invite["remaining-uses"] = int(data["remaining-uses"])
|
||||
else:
|
||||
invite["remaining-uses"] = 1
|
||||
log.debug(f"Creating new invite: {invite_code}")
|
||||
valid_till = current_time + delta
|
||||
invite["valid_till"] = valid_till.strftime("%Y-%m-%dT%H:%M:%S.%f")
|
||||
@@ -245,9 +283,20 @@ def getInvites():
|
||||
valid_for = expiry - current_time
|
||||
invite = {
|
||||
"code": code,
|
||||
"days": valid_for.days,
|
||||
"hours": valid_for.seconds // 3600,
|
||||
"minutes": (valid_for.seconds // 60) % 60,
|
||||
}
|
||||
if "created" in invites[code]:
|
||||
invite["created"] = invites[code]["created"]
|
||||
if "used-by" in invites[code]:
|
||||
invite["used-by"] = invites[code]["used-by"]
|
||||
if "no-limit" in invites[code]:
|
||||
invite["no-limit"] = invites[code]["no-limit"]
|
||||
if "remaining-uses" in invites[code]:
|
||||
invite["remaining-uses"] = invites[code]["remaining-uses"]
|
||||
else:
|
||||
invite["remaining-uses"] = 1
|
||||
if "email" in invites[code]:
|
||||
invite["email"] = invites[code]["email"]
|
||||
response["invites"].append(invite)
|
||||
|
||||
2553
package-lock.json
generated
2553
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
26
package.json
Normal file
26
package.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "jellyfin-accounts",
|
||||
"version": "1.0.0",
|
||||
"description": "This is only used for grabbing scss build dependencies, and isn't a real package.",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/hrfee/jellyfin-accounts.git"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"bugs": {
|
||||
"url": "https://github.com/hrfee/jellyfin-accounts/issues"
|
||||
},
|
||||
"homepage": "https://github.com/hrfee/jellyfin-accounts#readme",
|
||||
"dependencies": {
|
||||
"autoprefixer": "^9.8.4",
|
||||
"bootstrap": "^5.0.0-alpha1",
|
||||
"bootstrap4": "npm:bootstrap@^4.5.0",
|
||||
"clean-css-cli": "^4.3.0",
|
||||
"postcss-cli": "^7.1.1"
|
||||
}
|
||||
}
|
||||
43
poetry.lock
generated
43
poetry.lock
generated
@@ -162,6 +162,17 @@ MarkupSafe = ">=0.23"
|
||||
[package.extras]
|
||||
i18n = ["Babel (>=0.8)"]
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
description = "Sass for Python: A straightforward binding of libsass for Python."
|
||||
name = "libsass"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
version = "0.20.0"
|
||||
|
||||
[package.dependencies]
|
||||
six = "*"
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
description = "Safely add untrusted strings to HTML/XML markup."
|
||||
@@ -332,6 +343,17 @@ optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
|
||||
version = "1.15.0"
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
description = "tasks runner for python projects"
|
||||
name = "taskipy"
|
||||
optional = false
|
||||
python-versions = ">=3.6,<4.0"
|
||||
version = "1.2.1"
|
||||
|
||||
[package.dependencies]
|
||||
toml = ">=0.10.0,<0.11.0"
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
description = "Python Library for Tom's Obvious, Minimal Language"
|
||||
@@ -400,7 +422,7 @@ dev = ["pytest", "pytest-timeout", "coverage", "tox", "sphinx", "pallets-sphinx-
|
||||
watchdog = ["watchdog"]
|
||||
|
||||
[metadata]
|
||||
content-hash = "847ce2a6a3927efdfb3b78935b348e9b4dc63d7e60959af6cc8b9fbc5a24567b"
|
||||
content-hash = "fa8b5fb1ded41b673b8062a2bfc6467e6a484ff62b578147bec001d7d9d8ca16"
|
||||
python-versions = "^3.6"
|
||||
|
||||
[metadata.files]
|
||||
@@ -518,6 +540,21 @@ jinja2 = [
|
||||
{file = "Jinja2-2.11.2-py2.py3-none-any.whl", hash = "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035"},
|
||||
{file = "Jinja2-2.11.2.tar.gz", hash = "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0"},
|
||||
]
|
||||
libsass = [
|
||||
{file = "libsass-0.20.0-cp27-cp27m-macosx_10_14_intel.whl", hash = "sha256:107c409524c6a4ed14410fa9dafa9ee59c6bd3ecae75d73af749ab2b75685726"},
|
||||
{file = "libsass-0.20.0-cp27-cp27m-win32.whl", hash = "sha256:98f6dee9850b29e62977a963e3beb3cfeb98b128a267d59d2c3d675e298c8d57"},
|
||||
{file = "libsass-0.20.0-cp27-cp27m-win_amd64.whl", hash = "sha256:b077261a04ba1c213e932943208471972c5230222acb7fa97373e55a40872cbb"},
|
||||
{file = "libsass-0.20.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:e6a547c0aa731dcb4ed71f198e814bee0400ce04d553f3f12a53bc3a17f2a481"},
|
||||
{file = "libsass-0.20.0-cp36-abi3-manylinux1_x86_64.whl", hash = "sha256:74f6fb8da58179b5d86586bc045c16d93d55074bc7bb48b6354a4da7ac9f9dfd"},
|
||||
{file = "libsass-0.20.0-cp36-cp36m-win32.whl", hash = "sha256:a43f3830d83ad9a7f5013c05ce239ca71744d0780dad906587302ac5257bce60"},
|
||||
{file = "libsass-0.20.0-cp36-cp36m-win_amd64.whl", hash = "sha256:fd19c8f73f70ffc6cbcca8139da08ea9a71fc48e7dfc4bb236ad88ab2d6558f1"},
|
||||
{file = "libsass-0.20.0-cp37-abi3-macosx_10_14_x86_64.whl", hash = "sha256:8cf72552b39e78a1852132e16b706406bc76029fe3001583284ece8d8752a60a"},
|
||||
{file = "libsass-0.20.0-cp37-cp37m-win32.whl", hash = "sha256:7555d9b24e79943cfafac44dbb4ca7e62105c038de7c6b999838c9ff7b88645d"},
|
||||
{file = "libsass-0.20.0-cp37-cp37m-win_amd64.whl", hash = "sha256:794f4f4661667263e7feafe5cc866e3746c7c8a9192b2aa9afffdadcbc91c687"},
|
||||
{file = "libsass-0.20.0-cp38-cp38-win32.whl", hash = "sha256:3bc0d68778b30b5fa83199e18795314f64b26ca5871e026343e63934f616f7f7"},
|
||||
{file = "libsass-0.20.0-cp38-cp38-win_amd64.whl", hash = "sha256:5c8ff562b233734fbc72b23bb862cc6a6f70b1e9bf85a58422aa75108b94783b"},
|
||||
{file = "libsass-0.20.0.tar.gz", hash = "sha256:b7452f1df274b166dc22ee2e9154c4adca619bcbbdf8041a7aa05f372a1dacbc"},
|
||||
]
|
||||
markupsafe = [
|
||||
{file = "MarkupSafe-1.1.1-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161"},
|
||||
{file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"},
|
||||
@@ -645,6 +682,10 @@ six = [
|
||||
{file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"},
|
||||
{file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"},
|
||||
]
|
||||
taskipy = [
|
||||
{file = "taskipy-1.2.1-py3-none-any.whl", hash = "sha256:99bdaf5b19791c2345806847147e0fc2d28e1ac9446058def5a8b6b3fc9f23e2"},
|
||||
{file = "taskipy-1.2.1.tar.gz", hash = "sha256:5eb2c3b1606c896c7fa799848e71e8883b880759224958d07ba760e5db263175"},
|
||||
]
|
||||
toml = [
|
||||
{file = "toml-0.10.1-py2.py3-none-any.whl", hash = "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88"},
|
||||
{file = "toml-0.10.1.tar.gz", hash = "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f"},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "jellyfin-accounts"
|
||||
version = "0.3.0"
|
||||
version = "0.3.6"
|
||||
readme = "README.md"
|
||||
description = "A simple account management system for Jellyfin"
|
||||
authors = ["Harvey Tindall <harveyltindall@gmail.com>"]
|
||||
@@ -8,7 +8,7 @@ license = "MIT"
|
||||
homepage = "https://github.com/hrfee/jellyfin-accounts"
|
||||
repository = "https://github.com/hrfee/jellyfin-accounts"
|
||||
keywords = ["jellyfin", "jf-accounts"]
|
||||
include = ["jellyfin_accounts/data/*"]
|
||||
include = ["jellyfin_accounts/data/*", "jellyfin_accounts/data/static/*.css"]
|
||||
exclude = ["images/*", "scss/*"]
|
||||
classifiers = [
|
||||
"Programming Language :: Python :: 3",
|
||||
@@ -16,7 +16,6 @@ classifiers = [
|
||||
"Operating System :: OS Independent",
|
||||
]
|
||||
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.6"
|
||||
pyopenssl = "^19.1.0"
|
||||
@@ -34,11 +33,17 @@ packaging = "^20.4"
|
||||
[tool.poetry.dev-dependencies]
|
||||
neovim = "^0.3.1"
|
||||
black = "^19.10b0"
|
||||
|
||||
taskipy = "^1.2.1"
|
||||
libsass = "^0.20.0"
|
||||
|
||||
[tool.poetry.scripts]
|
||||
jf-accounts = 'jellyfin_accounts:main'
|
||||
|
||||
[tool.taskipy.tasks]
|
||||
pre_compile-css = "task get-npm-deps"
|
||||
compile-css = "python scss/compile.py"
|
||||
get-npm-deps = "python scss/get_node_deps.py"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry>=0.12"]
|
||||
build-backend = "poetry.masonry.api"
|
||||
|
||||
@@ -2,15 +2,9 @@
|
||||
|
||||
* `bs<4/5>-jf.scss` contains the source for the customizations to bootstrap. To customize the UI, you can make modifications to this file and then compile it.
|
||||
|
||||
**Note**: For BS5, it is assumed that bootstrap is installed in `../../node_modules/bootstrap` relative to itself.
|
||||
For BS4, it assumes that bootstrap is installed in `../../node_modules/bootstrap4` relative to itself (`npm install bootstrap4@npm:bootstrap`).
|
||||
* Compilation requires a sass compiler of your choice, and `postcss-cli`, `autoprefixer` + `clean-css-cli` from npm.
|
||||
* If you're using `sassc`, run `./compile.sh bs<4/5>-jf.scss` in this directory. This will create a .css file, and minified .css file.
|
||||
* For `node-sass`, replace the `sassc` line in `compile.sh` with
|
||||
```
|
||||
node-sass --output-style expanded --precision 6 $1 $css_file
|
||||
```
|
||||
and run as above.
|
||||
* If you're building from source, copy the minified css to `<jf-accounts git directory>/jellyfin_accounts/data/static/bs<4/5>-jf.css`.
|
||||
* If you're just customizing your install, set `custom_css` in your config as the path to your minified css and change the `theme` option to `Custom CSS`.
|
||||
**Note**: It is assumed that Bootstrap 5 is installed in `../../node_modules/bootstrap` relative to itself, and Bootstrap 4 in `../../node_modules/bootstrap4`.
|
||||
|
||||
* Compilation requires dev dependencies (`poetry update`), bootstrap and some extra npm packages.
|
||||
* If you're buildings from source, you can simply run `poetry run task compile-css` before building to automatically get deps and compile CSS.
|
||||
* If you are creating custom css, run `poetry run task get-npm-deps` to only install the necessary dependencies. Follow along with the commands `scss/compile.py` runs to build your css and then set `custom_css` in your config as the path to your minified css and change the `theme` option to `Custom CSS`.
|
||||
|
||||
|
||||
9548
scss/bs4/bs4-jf.css
9548
scss/bs4/bs4-jf.css
File diff suppressed because one or more lines are too long
7
scss/bs4/bs4-jf.min.css
vendored
7
scss/bs4/bs4-jf.min.css
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -120,6 +120,10 @@ $list-group-action-active-bg: $jf-blue-focus;
|
||||
color: $jf-text-bold;
|
||||
}
|
||||
|
||||
.icon-button {
|
||||
color: $text-muted;
|
||||
}
|
||||
|
||||
.text-bright {
|
||||
color: $jf-text-bold;
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
#!/bin/bash
|
||||
css_file=$(echo $1 | sed 's/scss/css/g')
|
||||
min_file=$(echo $1 | sed 's/scss/min.css/g')
|
||||
sassc -t expanded -p 6 $1 $css_file
|
||||
echo "Compiled."
|
||||
postcss $css_file --replace --use autoprefixer
|
||||
echo "Prefixed."
|
||||
echo "Written to $css_file."
|
||||
cleancss --level 1 --format breakWith=lf --source-map --source-map-inline-sources --output $min_file $css_file
|
||||
echo "Minified version written to $min_file."
|
||||
9438
scss/bs5/bs5-jf.css
9438
scss/bs5/bs5-jf.css
File diff suppressed because one or more lines are too long
7
scss/bs5/bs5-jf.min.css
vendored
7
scss/bs5/bs5-jf.min.css
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -120,6 +120,10 @@ $list-group-action-active-bg: $jf-blue-focus;
|
||||
color: $jf-text-bold;
|
||||
}
|
||||
|
||||
.icon-button:active {
|
||||
color: $text-muted;
|
||||
}
|
||||
|
||||
.text-bright {
|
||||
color: $jf-text-bold;
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
#!/bin/bash
|
||||
css_file=$(echo $1 | sed 's/scss/css/g')
|
||||
min_file=$(echo $1 | sed 's/scss/min.css/g')
|
||||
sassc -t expanded -p 6 $1 $css_file
|
||||
echo "Compiled."
|
||||
postcss $css_file --replace --use autoprefixer
|
||||
echo "Prefixed."
|
||||
echo "Written to $css_file."
|
||||
cleancss --level 1 --format breakWith=lf --source-map --source-map-inline-sources --output $min_file $css_file
|
||||
echo "Minified version written to $min_file."
|
||||
35
scss/compile.py
Executable file
35
scss/compile.py
Executable file
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env python3
|
||||
import sass
|
||||
import subprocess
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
def runcmd(cmd):
|
||||
proc = subprocess.Popen(cmd.split(), stdout=subprocess.PIPE)
|
||||
return proc.communicate()
|
||||
|
||||
local_path = Path(__file__).resolve().parent
|
||||
node_bin = local_path.parent / 'node_modules' / '.bin'
|
||||
|
||||
for bsv in [d for d in local_path.iterdir() if 'bs' in d.name]:
|
||||
scss = bsv / f'{bsv.name}-jf.scss'
|
||||
css = bsv / f'{bsv.name}-jf.css'
|
||||
min_css = bsv.parents[1] / 'jellyfin_accounts' / 'data' / 'static' / f'{bsv.name}-jf.css'
|
||||
with open(css, 'w') as f:
|
||||
f.write(sass.compile(filename=str(scss.resolve()),
|
||||
output_style='expanded',
|
||||
precision=6))
|
||||
if css.exists():
|
||||
print(f'{bsv.name}: Compiled.')
|
||||
runcmd(f'{str((node_bin / "postcss").resolve())} {str(css.resolve())} --replace --use autoprefixer')
|
||||
print(f'{bsv.name}: Prefixed.')
|
||||
runcmd(f'{str((node_bin / "cleancss").resolve())} --level 1 --format breakWith=lf --output {str(min_css.resolve())} {str(css.resolve())}')
|
||||
if min_css.exists():
|
||||
print(f'{bsv.name}: Minified and copied to {str(min_css.resolve())}.')
|
||||
|
||||
for v in [('bootstrap', 'bs5'), ('bootstrap4', 'bs4')]:
|
||||
new_path = str((local_path.parent / 'jellyfin_accounts' / 'data' / 'static' / (v[1] + '.css')).resolve())
|
||||
shutil.copy(str((local_path.parent / 'node_modules' / v[0] / 'dist' / 'css' / 'bootstrap.min.css').resolve()),
|
||||
new_path)
|
||||
print(f'Copied {v[1]} to {new_path}')
|
||||
|
||||
17
scss/get_node_deps.py
Normal file
17
scss/get_node_deps.py
Normal file
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
def runcmd(cmd):
|
||||
proc = subprocess.Popen(cmd.split(), stdout=subprocess.PIPE)
|
||||
return proc.communicate()
|
||||
|
||||
print('Installing npm packages')
|
||||
|
||||
root_path = Path(__file__).parents[1]
|
||||
runcmd(f'npm install --prefix {root_path}')
|
||||
|
||||
if (root_path / 'node_modules' / 'cleancss').exists():
|
||||
print(f'Installed successfully in {str((root_path / "node_modules").resolve())}.')
|
||||
|
||||
Reference in New Issue
Block a user