mirror of
https://github.com/Tautulli/Tautulli.git
synced 2025-08-20 21:33:18 -07:00
Merge remote-tracking branch 'upstream/nightly' into nightly
This commit is contained in:
commit
bc4f925d87
24 changed files with 1913 additions and 675 deletions
30
.github/workflows/publish-installers.yml
vendored
30
.github/workflows/publish-installers.yml
vendored
|
@ -125,41 +125,21 @@ jobs:
|
|||
echo "$EOF" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Create Release
|
||||
uses: actions/create-release@v1
|
||||
uses: softprops/action-gh-release@v1
|
||||
id: create_release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GHACTIONS_TOKEN }}
|
||||
with:
|
||||
tag_name: ${{ steps.get_version.outputs.RELEASE_VERSION }}
|
||||
release_name: Tautulli ${{ steps.get_version.outputs.RELEASE_VERSION }}
|
||||
name: Tautulli ${{ steps.get_version.outputs.RELEASE_VERSION }}
|
||||
body: |
|
||||
## Changelog
|
||||
|
||||
##${{ steps.get_changelog.outputs.CHANGELOG }}
|
||||
draft: false
|
||||
prerelease: ${{ endsWith(steps.get_version.outputs.RELEASE_VERSION, '-beta') }}
|
||||
|
||||
- name: Upload Windows Installer
|
||||
uses: actions/upload-release-asset@v1
|
||||
if: env.WORKFLOW_CONCLUSION == 'success'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GHACTIONS_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: Tautulli-windows-installer/Tautulli-windows-${{ steps.get_version.outputs.RELEASE_VERSION }}-x64.exe
|
||||
asset_name: Tautulli-windows-${{ steps.get_version.outputs.RELEASE_VERSION }}-x64.exe
|
||||
asset_content_type: application/vnd.microsoft.portable-executable
|
||||
|
||||
- name: Upload MacOS Installer
|
||||
uses: actions/upload-release-asset@v1
|
||||
if: env.WORKFLOW_CONCLUSION == 'success'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GHACTIONS_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: Tautulli-macos-installer/Tautulli-macos-${{ steps.get_version.outputs.RELEASE_VERSION }}-x64.pkg
|
||||
asset_name: Tautulli-macos-${{ steps.get_version.outputs.RELEASE_VERSION }}-x64.pkg
|
||||
asset_content_type: application/vnd.apple.installer+xml
|
||||
files: |
|
||||
Tautulli-windows-installer/Tautulli-windows-${{ steps.get_version.outputs.RELEASE_VERSION }}-x64.exe
|
||||
Tautulli-macos-installer/Tautulli-macos-${{ steps.get_version.outputs.RELEASE_VERSION }}-x64.pkg
|
||||
|
||||
discord:
|
||||
name: Discord Notification
|
||||
|
|
21
CHANGELOG.md
21
CHANGELOG.md
|
@ -1,6 +1,26 @@
|
|||
# Changelog
|
||||
|
||||
## v2.13.4 (2023-12-07)
|
||||
|
||||
* UI:
|
||||
* Fix: Tautulli configuration settings page not loading when system language is None.
|
||||
* Fix: Login cookie expiring too quickly.
|
||||
|
||||
|
||||
## v2.13.3 (2023-12-03)
|
||||
|
||||
* Notifications:
|
||||
* New: Added duration_time notification parameter.
|
||||
* New: Added file_size_bytes notification parameter.
|
||||
* New: Added time formats notification text modifiers.
|
||||
* New: Added support for thetvdb_url for movies.
|
||||
* UI:
|
||||
* Fix: Activity card overflowing due to screen scaling. (#2033)
|
||||
* Fix: Stream duration on activity card not being updated on track changes in some cases. (#2206)
|
||||
|
||||
|
||||
## v2.13.2 (2023-10-26)
|
||||
|
||||
* History:
|
||||
* New: Added quarter values icons for history watch status. (#2179, #2156) (Thanks @herby2212)
|
||||
* Graphs:
|
||||
|
@ -15,6 +35,7 @@
|
|||
|
||||
|
||||
## v2.13.1 (2023-08-25)
|
||||
|
||||
* Notes:
|
||||
* Support for Python 3.7 has been dropped. The minimum Python version is now 3.8.
|
||||
* Other:
|
||||
|
|
|
@ -24,10 +24,10 @@ import sys
|
|||
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), 'lib'))
|
||||
|
||||
|
||||
import appdirs
|
||||
import argparse
|
||||
import datetime
|
||||
import locale
|
||||
import platformdirs
|
||||
import pytz
|
||||
import signal
|
||||
import shutil
|
||||
|
@ -185,7 +185,7 @@ def main():
|
|||
if args.datadir:
|
||||
plexpy.DATA_DIR = args.datadir
|
||||
elif plexpy.FROZEN:
|
||||
plexpy.DATA_DIR = appdirs.user_data_dir("Tautulli", False)
|
||||
plexpy.DATA_DIR = platformdirs.user_data_dir("Tautulli", False)
|
||||
else:
|
||||
plexpy.DATA_DIR = plexpy.PROG_DIR
|
||||
|
||||
|
|
|
@ -74,7 +74,7 @@ DOCUMENTATION :: END
|
|||
</tr>
|
||||
<tr>
|
||||
<td>System Language:</td>
|
||||
<td>${plexpy.SYS_LANGUAGE + (' (override {})'.format(plexpy.CONFIG.PMS_LANGUAGE) if plexpy.CONFIG.PMS_LANGUAGE else '')}</td>
|
||||
<td>${plexpy.SYS_LANGUAGE}${' (override {})'.format(plexpy.CONFIG.PMS_LANGUAGE) if plexpy.CONFIG.PMS_LANGUAGE else ''}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Python Version:</td>
|
||||
|
|
|
@ -965,7 +965,7 @@ a .users-poster-face:hover {
|
|||
font-size: 10px;
|
||||
text-align: right;
|
||||
text-transform: uppercase;
|
||||
line-height: 14px;
|
||||
line-height: 10px;
|
||||
-webkit-flex-shrink: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
|
|
@ -562,6 +562,7 @@
|
|||
|
||||
// Update the stream progress times
|
||||
$('#stream-eta-' + key).html(moment().add(parseInt(s.duration) - parseInt(s.view_offset), 'milliseconds').format(time_format));
|
||||
$('#stream-duration-' + key).html(millisecondsToMinutes(parseInt(s.stream_duration), false));
|
||||
var stream_view_offset = $('#stream-view-offset-' + key);
|
||||
stream_view_offset.data('state', s.state);
|
||||
if (stream_view_offset.data('last_view_offset') !== s.view_offset) {
|
||||
|
|
|
@ -1923,6 +1923,21 @@
|
|||
<p><strong style="color: #fff;">Example:</strong></p>
|
||||
<pre>{media_type} --> movie
|
||||
{media_type!c} --> Movie</pre>
|
||||
</div>
|
||||
<div>
|
||||
<h4>Time Formats</h4>
|
||||
</div>
|
||||
<div style="padding-bottom: 10px;">
|
||||
<p class="help-block">
|
||||
Notification parameters which are "in date format" or "in time format" can be formatted using the
|
||||
<a href="javascript:void(0)" data-target="#dateTimeOptionsModal" data-toggle="modal">Date & Time Format Options</a>
|
||||
by adding a <span class="inline-pre">:format</span> specifier.
|
||||
If no format is specified, the default Date Format and Time Format under Settings > General will be used.
|
||||
</p>
|
||||
<p><strong style="color: #fff;">Example:</strong></p>
|
||||
<pre>{started_datestamp:ddd, MMMM DD, YYYY} --> Mon, December 25, 2023
|
||||
{stopped_timestamp:h:mm a} --> 9:56 pm
|
||||
{duration_time:HH:mm:ss} --> 01:42:20</pre>
|
||||
</div>
|
||||
<div>
|
||||
<h4>List Slicing</h4>
|
||||
|
@ -1994,7 +2009,7 @@ Rating: {rating}/10 --> Rating: /10
|
|||
<li>Evaluation</li>
|
||||
<li>Parameter</li>
|
||||
<li>Case Modifier</li>
|
||||
<li>List Slicing</li>
|
||||
<li>Time Formats / List Slicing</li>
|
||||
<li>Suffix</li>
|
||||
</ol>
|
||||
<p><strong style="color: #fff;">Example:</strong></p>
|
||||
|
|
608
lib/appdirs.py
608
lib/appdirs.py
|
@ -1,608 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2005-2010 ActiveState Software Inc.
|
||||
# Copyright (c) 2013 Eddy Petrișor
|
||||
|
||||
"""Utilities for determining application-specific dirs.
|
||||
|
||||
See <http://github.com/ActiveState/appdirs> for details and usage.
|
||||
"""
|
||||
# Dev Notes:
|
||||
# - MSDN on where to store app data files:
|
||||
# http://support.microsoft.com/default.aspx?scid=kb;en-us;310294#XSLTH3194121123120121120120
|
||||
# - Mac OS X: http://developer.apple.com/documentation/MacOSX/Conceptual/BPFileSystem/index.html
|
||||
# - XDG spec for Un*x: http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html
|
||||
|
||||
__version__ = "1.4.4"
|
||||
__version_info__ = tuple(int(segment) for segment in __version__.split("."))
|
||||
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
PY3 = sys.version_info[0] == 3
|
||||
|
||||
if PY3:
|
||||
unicode = str
|
||||
|
||||
if sys.platform.startswith('java'):
|
||||
import platform
|
||||
os_name = platform.java_ver()[3][0]
|
||||
if os_name.startswith('Windows'): # "Windows XP", "Windows 7", etc.
|
||||
system = 'win32'
|
||||
elif os_name.startswith('Mac'): # "Mac OS X", etc.
|
||||
system = 'darwin'
|
||||
else: # "Linux", "SunOS", "FreeBSD", etc.
|
||||
# Setting this to "linux2" is not ideal, but only Windows or Mac
|
||||
# are actually checked for and the rest of the module expects
|
||||
# *sys.platform* style strings.
|
||||
system = 'linux2'
|
||||
else:
|
||||
system = sys.platform
|
||||
|
||||
|
||||
|
||||
def user_data_dir(appname=None, appauthor=None, version=None, roaming=False):
|
||||
r"""Return full path to the user-specific data dir for this application.
|
||||
|
||||
"appname" is the name of application.
|
||||
If None, just the system directory is returned.
|
||||
"appauthor" (only used on Windows) is the name of the
|
||||
appauthor or distributing body for this application. Typically
|
||||
it is the owning company name. This falls back to appname. You may
|
||||
pass False to disable it.
|
||||
"version" is an optional version path element to append to the
|
||||
path. You might want to use this if you want multiple versions
|
||||
of your app to be able to run independently. If used, this
|
||||
would typically be "<major>.<minor>".
|
||||
Only applied when appname is present.
|
||||
"roaming" (boolean, default False) can be set True to use the Windows
|
||||
roaming appdata directory. That means that for users on a Windows
|
||||
network setup for roaming profiles, this user data will be
|
||||
sync'd on login. See
|
||||
<http://technet.microsoft.com/en-us/library/cc766489(WS.10).aspx>
|
||||
for a discussion of issues.
|
||||
|
||||
Typical user data directories are:
|
||||
Mac OS X: ~/Library/Application Support/<AppName>
|
||||
Unix: ~/.local/share/<AppName> # or in $XDG_DATA_HOME, if defined
|
||||
Win XP (not roaming): C:\Documents and Settings\<username>\Application Data\<AppAuthor>\<AppName>
|
||||
Win XP (roaming): C:\Documents and Settings\<username>\Local Settings\Application Data\<AppAuthor>\<AppName>
|
||||
Win 7 (not roaming): C:\Users\<username>\AppData\Local\<AppAuthor>\<AppName>
|
||||
Win 7 (roaming): C:\Users\<username>\AppData\Roaming\<AppAuthor>\<AppName>
|
||||
|
||||
For Unix, we follow the XDG spec and support $XDG_DATA_HOME.
|
||||
That means, by default "~/.local/share/<AppName>".
|
||||
"""
|
||||
if system == "win32":
|
||||
if appauthor is None:
|
||||
appauthor = appname
|
||||
const = roaming and "CSIDL_APPDATA" or "CSIDL_LOCAL_APPDATA"
|
||||
path = os.path.normpath(_get_win_folder(const))
|
||||
if appname:
|
||||
if appauthor is not False:
|
||||
path = os.path.join(path, appauthor, appname)
|
||||
else:
|
||||
path = os.path.join(path, appname)
|
||||
elif system == 'darwin':
|
||||
path = os.path.expanduser('~/Library/Application Support/')
|
||||
if appname:
|
||||
path = os.path.join(path, appname)
|
||||
else:
|
||||
path = os.getenv('XDG_DATA_HOME', os.path.expanduser("~/.local/share"))
|
||||
if appname:
|
||||
path = os.path.join(path, appname)
|
||||
if appname and version:
|
||||
path = os.path.join(path, version)
|
||||
return path
|
||||
|
||||
|
||||
def site_data_dir(appname=None, appauthor=None, version=None, multipath=False):
|
||||
r"""Return full path to the user-shared data dir for this application.
|
||||
|
||||
"appname" is the name of application.
|
||||
If None, just the system directory is returned.
|
||||
"appauthor" (only used on Windows) is the name of the
|
||||
appauthor or distributing body for this application. Typically
|
||||
it is the owning company name. This falls back to appname. You may
|
||||
pass False to disable it.
|
||||
"version" is an optional version path element to append to the
|
||||
path. You might want to use this if you want multiple versions
|
||||
of your app to be able to run independently. If used, this
|
||||
would typically be "<major>.<minor>".
|
||||
Only applied when appname is present.
|
||||
"multipath" is an optional parameter only applicable to *nix
|
||||
which indicates that the entire list of data dirs should be
|
||||
returned. By default, the first item from XDG_DATA_DIRS is
|
||||
returned, or '/usr/local/share/<AppName>',
|
||||
if XDG_DATA_DIRS is not set
|
||||
|
||||
Typical site data directories are:
|
||||
Mac OS X: /Library/Application Support/<AppName>
|
||||
Unix: /usr/local/share/<AppName> or /usr/share/<AppName>
|
||||
Win XP: C:\Documents and Settings\All Users\Application Data\<AppAuthor>\<AppName>
|
||||
Vista: (Fail! "C:\ProgramData" is a hidden *system* directory on Vista.)
|
||||
Win 7: C:\ProgramData\<AppAuthor>\<AppName> # Hidden, but writeable on Win 7.
|
||||
|
||||
For Unix, this is using the $XDG_DATA_DIRS[0] default.
|
||||
|
||||
WARNING: Do not use this on Windows. See the Vista-Fail note above for why.
|
||||
"""
|
||||
if system == "win32":
|
||||
if appauthor is None:
|
||||
appauthor = appname
|
||||
path = os.path.normpath(_get_win_folder("CSIDL_COMMON_APPDATA"))
|
||||
if appname:
|
||||
if appauthor is not False:
|
||||
path = os.path.join(path, appauthor, appname)
|
||||
else:
|
||||
path = os.path.join(path, appname)
|
||||
elif system == 'darwin':
|
||||
path = os.path.expanduser('/Library/Application Support')
|
||||
if appname:
|
||||
path = os.path.join(path, appname)
|
||||
else:
|
||||
# XDG default for $XDG_DATA_DIRS
|
||||
# only first, if multipath is False
|
||||
path = os.getenv('XDG_DATA_DIRS',
|
||||
os.pathsep.join(['/usr/local/share', '/usr/share']))
|
||||
pathlist = [os.path.expanduser(x.rstrip(os.sep)) for x in path.split(os.pathsep)]
|
||||
if appname:
|
||||
if version:
|
||||
appname = os.path.join(appname, version)
|
||||
pathlist = [os.sep.join([x, appname]) for x in pathlist]
|
||||
|
||||
if multipath:
|
||||
path = os.pathsep.join(pathlist)
|
||||
else:
|
||||
path = pathlist[0]
|
||||
return path
|
||||
|
||||
if appname and version:
|
||||
path = os.path.join(path, version)
|
||||
return path
|
||||
|
||||
|
||||
def user_config_dir(appname=None, appauthor=None, version=None, roaming=False):
|
||||
r"""Return full path to the user-specific config dir for this application.
|
||||
|
||||
"appname" is the name of application.
|
||||
If None, just the system directory is returned.
|
||||
"appauthor" (only used on Windows) is the name of the
|
||||
appauthor or distributing body for this application. Typically
|
||||
it is the owning company name. This falls back to appname. You may
|
||||
pass False to disable it.
|
||||
"version" is an optional version path element to append to the
|
||||
path. You might want to use this if you want multiple versions
|
||||
of your app to be able to run independently. If used, this
|
||||
would typically be "<major>.<minor>".
|
||||
Only applied when appname is present.
|
||||
"roaming" (boolean, default False) can be set True to use the Windows
|
||||
roaming appdata directory. That means that for users on a Windows
|
||||
network setup for roaming profiles, this user data will be
|
||||
sync'd on login. See
|
||||
<http://technet.microsoft.com/en-us/library/cc766489(WS.10).aspx>
|
||||
for a discussion of issues.
|
||||
|
||||
Typical user config directories are:
|
||||
Mac OS X: same as user_data_dir
|
||||
Unix: ~/.config/<AppName> # or in $XDG_CONFIG_HOME, if defined
|
||||
Win *: same as user_data_dir
|
||||
|
||||
For Unix, we follow the XDG spec and support $XDG_CONFIG_HOME.
|
||||
That means, by default "~/.config/<AppName>".
|
||||
"""
|
||||
if system in ["win32", "darwin"]:
|
||||
path = user_data_dir(appname, appauthor, None, roaming)
|
||||
else:
|
||||
path = os.getenv('XDG_CONFIG_HOME', os.path.expanduser("~/.config"))
|
||||
if appname:
|
||||
path = os.path.join(path, appname)
|
||||
if appname and version:
|
||||
path = os.path.join(path, version)
|
||||
return path
|
||||
|
||||
|
||||
def site_config_dir(appname=None, appauthor=None, version=None, multipath=False):
|
||||
r"""Return full path to the user-shared data dir for this application.
|
||||
|
||||
"appname" is the name of application.
|
||||
If None, just the system directory is returned.
|
||||
"appauthor" (only used on Windows) is the name of the
|
||||
appauthor or distributing body for this application. Typically
|
||||
it is the owning company name. This falls back to appname. You may
|
||||
pass False to disable it.
|
||||
"version" is an optional version path element to append to the
|
||||
path. You might want to use this if you want multiple versions
|
||||
of your app to be able to run independently. If used, this
|
||||
would typically be "<major>.<minor>".
|
||||
Only applied when appname is present.
|
||||
"multipath" is an optional parameter only applicable to *nix
|
||||
which indicates that the entire list of config dirs should be
|
||||
returned. By default, the first item from XDG_CONFIG_DIRS is
|
||||
returned, or '/etc/xdg/<AppName>', if XDG_CONFIG_DIRS is not set
|
||||
|
||||
Typical site config directories are:
|
||||
Mac OS X: same as site_data_dir
|
||||
Unix: /etc/xdg/<AppName> or $XDG_CONFIG_DIRS[i]/<AppName> for each value in
|
||||
$XDG_CONFIG_DIRS
|
||||
Win *: same as site_data_dir
|
||||
Vista: (Fail! "C:\ProgramData" is a hidden *system* directory on Vista.)
|
||||
|
||||
For Unix, this is using the $XDG_CONFIG_DIRS[0] default, if multipath=False
|
||||
|
||||
WARNING: Do not use this on Windows. See the Vista-Fail note above for why.
|
||||
"""
|
||||
if system in ["win32", "darwin"]:
|
||||
path = site_data_dir(appname, appauthor)
|
||||
if appname and version:
|
||||
path = os.path.join(path, version)
|
||||
else:
|
||||
# XDG default for $XDG_CONFIG_DIRS
|
||||
# only first, if multipath is False
|
||||
path = os.getenv('XDG_CONFIG_DIRS', '/etc/xdg')
|
||||
pathlist = [os.path.expanduser(x.rstrip(os.sep)) for x in path.split(os.pathsep)]
|
||||
if appname:
|
||||
if version:
|
||||
appname = os.path.join(appname, version)
|
||||
pathlist = [os.sep.join([x, appname]) for x in pathlist]
|
||||
|
||||
if multipath:
|
||||
path = os.pathsep.join(pathlist)
|
||||
else:
|
||||
path = pathlist[0]
|
||||
return path
|
||||
|
||||
|
||||
def user_cache_dir(appname=None, appauthor=None, version=None, opinion=True):
|
||||
r"""Return full path to the user-specific cache dir for this application.
|
||||
|
||||
"appname" is the name of application.
|
||||
If None, just the system directory is returned.
|
||||
"appauthor" (only used on Windows) is the name of the
|
||||
appauthor or distributing body for this application. Typically
|
||||
it is the owning company name. This falls back to appname. You may
|
||||
pass False to disable it.
|
||||
"version" is an optional version path element to append to the
|
||||
path. You might want to use this if you want multiple versions
|
||||
of your app to be able to run independently. If used, this
|
||||
would typically be "<major>.<minor>".
|
||||
Only applied when appname is present.
|
||||
"opinion" (boolean) can be False to disable the appending of
|
||||
"Cache" to the base app data dir for Windows. See
|
||||
discussion below.
|
||||
|
||||
Typical user cache directories are:
|
||||
Mac OS X: ~/Library/Caches/<AppName>
|
||||
Unix: ~/.cache/<AppName> (XDG default)
|
||||
Win XP: C:\Documents and Settings\<username>\Local Settings\Application Data\<AppAuthor>\<AppName>\Cache
|
||||
Vista: C:\Users\<username>\AppData\Local\<AppAuthor>\<AppName>\Cache
|
||||
|
||||
On Windows the only suggestion in the MSDN docs is that local settings go in
|
||||
the `CSIDL_LOCAL_APPDATA` directory. This is identical to the non-roaming
|
||||
app data dir (the default returned by `user_data_dir` above). Apps typically
|
||||
put cache data somewhere *under* the given dir here. Some examples:
|
||||
...\Mozilla\Firefox\Profiles\<ProfileName>\Cache
|
||||
...\Acme\SuperApp\Cache\1.0
|
||||
OPINION: This function appends "Cache" to the `CSIDL_LOCAL_APPDATA` value.
|
||||
This can be disabled with the `opinion=False` option.
|
||||
"""
|
||||
if system == "win32":
|
||||
if appauthor is None:
|
||||
appauthor = appname
|
||||
path = os.path.normpath(_get_win_folder("CSIDL_LOCAL_APPDATA"))
|
||||
if appname:
|
||||
if appauthor is not False:
|
||||
path = os.path.join(path, appauthor, appname)
|
||||
else:
|
||||
path = os.path.join(path, appname)
|
||||
if opinion:
|
||||
path = os.path.join(path, "Cache")
|
||||
elif system == 'darwin':
|
||||
path = os.path.expanduser('~/Library/Caches')
|
||||
if appname:
|
||||
path = os.path.join(path, appname)
|
||||
else:
|
||||
path = os.getenv('XDG_CACHE_HOME', os.path.expanduser('~/.cache'))
|
||||
if appname:
|
||||
path = os.path.join(path, appname)
|
||||
if appname and version:
|
||||
path = os.path.join(path, version)
|
||||
return path
|
||||
|
||||
|
||||
def user_state_dir(appname=None, appauthor=None, version=None, roaming=False):
|
||||
r"""Return full path to the user-specific state dir for this application.
|
||||
|
||||
"appname" is the name of application.
|
||||
If None, just the system directory is returned.
|
||||
"appauthor" (only used on Windows) is the name of the
|
||||
appauthor or distributing body for this application. Typically
|
||||
it is the owning company name. This falls back to appname. You may
|
||||
pass False to disable it.
|
||||
"version" is an optional version path element to append to the
|
||||
path. You might want to use this if you want multiple versions
|
||||
of your app to be able to run independently. If used, this
|
||||
would typically be "<major>.<minor>".
|
||||
Only applied when appname is present.
|
||||
"roaming" (boolean, default False) can be set True to use the Windows
|
||||
roaming appdata directory. That means that for users on a Windows
|
||||
network setup for roaming profiles, this user data will be
|
||||
sync'd on login. See
|
||||
<http://technet.microsoft.com/en-us/library/cc766489(WS.10).aspx>
|
||||
for a discussion of issues.
|
||||
|
||||
Typical user state directories are:
|
||||
Mac OS X: same as user_data_dir
|
||||
Unix: ~/.local/state/<AppName> # or in $XDG_STATE_HOME, if defined
|
||||
Win *: same as user_data_dir
|
||||
|
||||
For Unix, we follow this Debian proposal <https://wiki.debian.org/XDGBaseDirectorySpecification#state>
|
||||
to extend the XDG spec and support $XDG_STATE_HOME.
|
||||
|
||||
That means, by default "~/.local/state/<AppName>".
|
||||
"""
|
||||
if system in ["win32", "darwin"]:
|
||||
path = user_data_dir(appname, appauthor, None, roaming)
|
||||
else:
|
||||
path = os.getenv('XDG_STATE_HOME', os.path.expanduser("~/.local/state"))
|
||||
if appname:
|
||||
path = os.path.join(path, appname)
|
||||
if appname and version:
|
||||
path = os.path.join(path, version)
|
||||
return path
|
||||
|
||||
|
||||
def user_log_dir(appname=None, appauthor=None, version=None, opinion=True):
|
||||
r"""Return full path to the user-specific log dir for this application.
|
||||
|
||||
"appname" is the name of application.
|
||||
If None, just the system directory is returned.
|
||||
"appauthor" (only used on Windows) is the name of the
|
||||
appauthor or distributing body for this application. Typically
|
||||
it is the owning company name. This falls back to appname. You may
|
||||
pass False to disable it.
|
||||
"version" is an optional version path element to append to the
|
||||
path. You might want to use this if you want multiple versions
|
||||
of your app to be able to run independently. If used, this
|
||||
would typically be "<major>.<minor>".
|
||||
Only applied when appname is present.
|
||||
"opinion" (boolean) can be False to disable the appending of
|
||||
"Logs" to the base app data dir for Windows, and "log" to the
|
||||
base cache dir for Unix. See discussion below.
|
||||
|
||||
Typical user log directories are:
|
||||
Mac OS X: ~/Library/Logs/<AppName>
|
||||
Unix: ~/.cache/<AppName>/log # or under $XDG_CACHE_HOME if defined
|
||||
Win XP: C:\Documents and Settings\<username>\Local Settings\Application Data\<AppAuthor>\<AppName>\Logs
|
||||
Vista: C:\Users\<username>\AppData\Local\<AppAuthor>\<AppName>\Logs
|
||||
|
||||
On Windows the only suggestion in the MSDN docs is that local settings
|
||||
go in the `CSIDL_LOCAL_APPDATA` directory. (Note: I'm interested in
|
||||
examples of what some windows apps use for a logs dir.)
|
||||
|
||||
OPINION: This function appends "Logs" to the `CSIDL_LOCAL_APPDATA`
|
||||
value for Windows and appends "log" to the user cache dir for Unix.
|
||||
This can be disabled with the `opinion=False` option.
|
||||
"""
|
||||
if system == "darwin":
|
||||
path = os.path.join(
|
||||
os.path.expanduser('~/Library/Logs'),
|
||||
appname)
|
||||
elif system == "win32":
|
||||
path = user_data_dir(appname, appauthor, version)
|
||||
version = False
|
||||
if opinion:
|
||||
path = os.path.join(path, "Logs")
|
||||
else:
|
||||
path = user_cache_dir(appname, appauthor, version)
|
||||
version = False
|
||||
if opinion:
|
||||
path = os.path.join(path, "log")
|
||||
if appname and version:
|
||||
path = os.path.join(path, version)
|
||||
return path
|
||||
|
||||
|
||||
class AppDirs(object):
|
||||
"""Convenience wrapper for getting application dirs."""
|
||||
def __init__(self, appname=None, appauthor=None, version=None,
|
||||
roaming=False, multipath=False):
|
||||
self.appname = appname
|
||||
self.appauthor = appauthor
|
||||
self.version = version
|
||||
self.roaming = roaming
|
||||
self.multipath = multipath
|
||||
|
||||
@property
|
||||
def user_data_dir(self):
|
||||
return user_data_dir(self.appname, self.appauthor,
|
||||
version=self.version, roaming=self.roaming)
|
||||
|
||||
@property
|
||||
def site_data_dir(self):
|
||||
return site_data_dir(self.appname, self.appauthor,
|
||||
version=self.version, multipath=self.multipath)
|
||||
|
||||
@property
|
||||
def user_config_dir(self):
|
||||
return user_config_dir(self.appname, self.appauthor,
|
||||
version=self.version, roaming=self.roaming)
|
||||
|
||||
@property
|
||||
def site_config_dir(self):
|
||||
return site_config_dir(self.appname, self.appauthor,
|
||||
version=self.version, multipath=self.multipath)
|
||||
|
||||
@property
|
||||
def user_cache_dir(self):
|
||||
return user_cache_dir(self.appname, self.appauthor,
|
||||
version=self.version)
|
||||
|
||||
@property
|
||||
def user_state_dir(self):
|
||||
return user_state_dir(self.appname, self.appauthor,
|
||||
version=self.version)
|
||||
|
||||
@property
|
||||
def user_log_dir(self):
|
||||
return user_log_dir(self.appname, self.appauthor,
|
||||
version=self.version)
|
||||
|
||||
|
||||
#---- internal support stuff
|
||||
|
||||
def _get_win_folder_from_registry(csidl_name):
|
||||
"""This is a fallback technique at best. I'm not sure if using the
|
||||
registry for this guarantees us the correct answer for all CSIDL_*
|
||||
names.
|
||||
"""
|
||||
if PY3:
|
||||
import winreg as _winreg
|
||||
else:
|
||||
import _winreg
|
||||
|
||||
shell_folder_name = {
|
||||
"CSIDL_APPDATA": "AppData",
|
||||
"CSIDL_COMMON_APPDATA": "Common AppData",
|
||||
"CSIDL_LOCAL_APPDATA": "Local AppData",
|
||||
}[csidl_name]
|
||||
|
||||
key = _winreg.OpenKey(
|
||||
_winreg.HKEY_CURRENT_USER,
|
||||
r"Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders"
|
||||
)
|
||||
dir, type = _winreg.QueryValueEx(key, shell_folder_name)
|
||||
return dir
|
||||
|
||||
|
||||
def _get_win_folder_with_pywin32(csidl_name):
|
||||
from win32com.shell import shellcon, shell
|
||||
dir = shell.SHGetFolderPath(0, getattr(shellcon, csidl_name), 0, 0)
|
||||
# Try to make this a unicode path because SHGetFolderPath does
|
||||
# not return unicode strings when there is unicode data in the
|
||||
# path.
|
||||
try:
|
||||
dir = unicode(dir)
|
||||
|
||||
# Downgrade to short path name if have highbit chars. See
|
||||
# <http://bugs.activestate.com/show_bug.cgi?id=85099>.
|
||||
has_high_char = False
|
||||
for c in dir:
|
||||
if ord(c) > 255:
|
||||
has_high_char = True
|
||||
break
|
||||
if has_high_char:
|
||||
try:
|
||||
import win32api
|
||||
dir = win32api.GetShortPathName(dir)
|
||||
except ImportError:
|
||||
pass
|
||||
except UnicodeError:
|
||||
pass
|
||||
return dir
|
||||
|
||||
|
||||
def _get_win_folder_with_ctypes(csidl_name):
|
||||
import ctypes
|
||||
|
||||
csidl_const = {
|
||||
"CSIDL_APPDATA": 26,
|
||||
"CSIDL_COMMON_APPDATA": 35,
|
||||
"CSIDL_LOCAL_APPDATA": 28,
|
||||
}[csidl_name]
|
||||
|
||||
buf = ctypes.create_unicode_buffer(1024)
|
||||
ctypes.windll.shell32.SHGetFolderPathW(None, csidl_const, None, 0, buf)
|
||||
|
||||
# Downgrade to short path name if have highbit chars. See
|
||||
# <http://bugs.activestate.com/show_bug.cgi?id=85099>.
|
||||
has_high_char = False
|
||||
for c in buf:
|
||||
if ord(c) > 255:
|
||||
has_high_char = True
|
||||
break
|
||||
if has_high_char:
|
||||
buf2 = ctypes.create_unicode_buffer(1024)
|
||||
if ctypes.windll.kernel32.GetShortPathNameW(buf.value, buf2, 1024):
|
||||
buf = buf2
|
||||
|
||||
return buf.value
|
||||
|
||||
def _get_win_folder_with_jna(csidl_name):
|
||||
import array
|
||||
from com.sun import jna
|
||||
from com.sun.jna.platform import win32
|
||||
|
||||
buf_size = win32.WinDef.MAX_PATH * 2
|
||||
buf = array.zeros('c', buf_size)
|
||||
shell = win32.Shell32.INSTANCE
|
||||
shell.SHGetFolderPath(None, getattr(win32.ShlObj, csidl_name), None, win32.ShlObj.SHGFP_TYPE_CURRENT, buf)
|
||||
dir = jna.Native.toString(buf.tostring()).rstrip("\0")
|
||||
|
||||
# Downgrade to short path name if have highbit chars. See
|
||||
# <http://bugs.activestate.com/show_bug.cgi?id=85099>.
|
||||
has_high_char = False
|
||||
for c in dir:
|
||||
if ord(c) > 255:
|
||||
has_high_char = True
|
||||
break
|
||||
if has_high_char:
|
||||
buf = array.zeros('c', buf_size)
|
||||
kernel = win32.Kernel32.INSTANCE
|
||||
if kernel.GetShortPathName(dir, buf, buf_size):
|
||||
dir = jna.Native.toString(buf.tostring()).rstrip("\0")
|
||||
|
||||
return dir
|
||||
|
||||
if system == "win32":
|
||||
try:
|
||||
import win32com.shell
|
||||
_get_win_folder = _get_win_folder_with_pywin32
|
||||
except ImportError:
|
||||
try:
|
||||
from ctypes import windll
|
||||
_get_win_folder = _get_win_folder_with_ctypes
|
||||
except ImportError:
|
||||
try:
|
||||
import com.sun.jna
|
||||
_get_win_folder = _get_win_folder_with_jna
|
||||
except ImportError:
|
||||
_get_win_folder = _get_win_folder_from_registry
|
||||
|
||||
|
||||
#---- self test code
|
||||
|
||||
if __name__ == "__main__":
|
||||
appname = "MyApp"
|
||||
appauthor = "MyCompany"
|
||||
|
||||
props = ("user_data_dir",
|
||||
"user_config_dir",
|
||||
"user_cache_dir",
|
||||
"user_state_dir",
|
||||
"user_log_dir",
|
||||
"site_data_dir",
|
||||
"site_config_dir")
|
||||
|
||||
print("-- app dirs %s --" % __version__)
|
||||
|
||||
print("-- app dirs (with optional 'version')")
|
||||
dirs = AppDirs(appname, appauthor, version="1.0")
|
||||
for prop in props:
|
||||
print("%s: %s" % (prop, getattr(dirs, prop)))
|
||||
|
||||
print("\n-- app dirs (without optional 'version')")
|
||||
dirs = AppDirs(appname, appauthor)
|
||||
for prop in props:
|
||||
print("%s: %s" % (prop, getattr(dirs, prop)))
|
||||
|
||||
print("\n-- app dirs (without optional 'appauthor')")
|
||||
dirs = AppDirs(appname)
|
||||
for prop in props:
|
||||
print("%s: %s" % (prop, getattr(dirs, prop)))
|
||||
|
||||
print("\n-- app dirs (with disabled 'appauthor')")
|
||||
dirs = AppDirs(appname, appauthor=False)
|
||||
for prop in props:
|
||||
print("%s: %s" % (prop, getattr(dirs, prop)))
|
628
lib/platformdirs/__init__.py
Normal file
628
lib/platformdirs/__init__.py
Normal file
|
@ -0,0 +1,628 @@
|
|||
"""
|
||||
Utilities for determining application-specific dirs. See <https://github.com/platformdirs/platformdirs> for details and
|
||||
usage.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from .api import PlatformDirsABC
|
||||
from .version import __version__
|
||||
from .version import __version_tuple__ as __version_info__
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pathlib import Path
|
||||
|
||||
if sys.version_info >= (3, 8): # pragma: no cover (py38+)
|
||||
from typing import Literal
|
||||
else: # pragma: no cover (py38+)
|
||||
from typing_extensions import Literal
|
||||
|
||||
|
||||
def _set_platform_dir_class() -> type[PlatformDirsABC]:
|
||||
if sys.platform == "win32":
|
||||
from platformdirs.windows import Windows as Result
|
||||
elif sys.platform == "darwin":
|
||||
from platformdirs.macos import MacOS as Result
|
||||
else:
|
||||
from platformdirs.unix import Unix as Result
|
||||
|
||||
if os.getenv("ANDROID_DATA") == "/data" and os.getenv("ANDROID_ROOT") == "/system":
|
||||
if os.getenv("SHELL") or os.getenv("PREFIX"):
|
||||
return Result
|
||||
|
||||
from platformdirs.android import _android_folder
|
||||
|
||||
if _android_folder() is not None:
|
||||
from platformdirs.android import Android
|
||||
|
||||
return Android # return to avoid redefinition of result
|
||||
|
||||
return Result
|
||||
|
||||
|
||||
PlatformDirs = _set_platform_dir_class() #: Currently active platform
|
||||
AppDirs = PlatformDirs #: Backwards compatibility with appdirs
|
||||
|
||||
|
||||
def user_data_dir(
|
||||
appname: str | None = None,
|
||||
appauthor: str | None | Literal[False] = None,
|
||||
version: str | None = None,
|
||||
roaming: bool = False, # noqa: FBT001, FBT002
|
||||
ensure_exists: bool = False, # noqa: FBT001, FBT002
|
||||
) -> str:
|
||||
"""
|
||||
:param appname: See `appname <platformdirs.api.PlatformDirsABC.appname>`.
|
||||
:param appauthor: See `appauthor <platformdirs.api.PlatformDirsABC.appauthor>`.
|
||||
:param version: See `version <platformdirs.api.PlatformDirsABC.version>`.
|
||||
:param roaming: See `roaming <platformdirs.api.PlatformDirsABC.roaming>`.
|
||||
:param ensure_exists: See `ensure_exists <platformdirs.api.PlatformDirsABC.ensure_exists>`.
|
||||
:returns: data directory tied to the user
|
||||
"""
|
||||
return PlatformDirs(
|
||||
appname=appname,
|
||||
appauthor=appauthor,
|
||||
version=version,
|
||||
roaming=roaming,
|
||||
ensure_exists=ensure_exists,
|
||||
).user_data_dir
|
||||
|
||||
|
||||
def site_data_dir(
|
||||
appname: str | None = None,
|
||||
appauthor: str | None | Literal[False] = None,
|
||||
version: str | None = None,
|
||||
multipath: bool = False, # noqa: FBT001, FBT002
|
||||
ensure_exists: bool = False, # noqa: FBT001, FBT002
|
||||
) -> str:
|
||||
"""
|
||||
:param appname: See `appname <platformdirs.api.PlatformDirsABC.appname>`.
|
||||
:param appauthor: See `appauthor <platformdirs.api.PlatformDirsABC.appauthor>`.
|
||||
:param version: See `version <platformdirs.api.PlatformDirsABC.version>`.
|
||||
:param multipath: See `roaming <platformdirs.api.PlatformDirsABC.multipath>`.
|
||||
:param ensure_exists: See `ensure_exists <platformdirs.api.PlatformDirsABC.ensure_exists>`.
|
||||
:returns: data directory shared by users
|
||||
"""
|
||||
return PlatformDirs(
|
||||
appname=appname,
|
||||
appauthor=appauthor,
|
||||
version=version,
|
||||
multipath=multipath,
|
||||
ensure_exists=ensure_exists,
|
||||
).site_data_dir
|
||||
|
||||
|
||||
def user_config_dir(
|
||||
appname: str | None = None,
|
||||
appauthor: str | None | Literal[False] = None,
|
||||
version: str | None = None,
|
||||
roaming: bool = False, # noqa: FBT001, FBT002
|
||||
ensure_exists: bool = False, # noqa: FBT001, FBT002
|
||||
) -> str:
|
||||
"""
|
||||
:param appname: See `appname <platformdirs.api.PlatformDirsABC.appname>`.
|
||||
:param appauthor: See `appauthor <platformdirs.api.PlatformDirsABC.appauthor>`.
|
||||
:param version: See `version <platformdirs.api.PlatformDirsABC.version>`.
|
||||
:param roaming: See `roaming <platformdirs.api.PlatformDirsABC.roaming>`.
|
||||
:param ensure_exists: See `ensure_exists <platformdirs.api.PlatformDirsABC.ensure_exists>`.
|
||||
:returns: config directory tied to the user
|
||||
"""
|
||||
return PlatformDirs(
|
||||
appname=appname,
|
||||
appauthor=appauthor,
|
||||
version=version,
|
||||
roaming=roaming,
|
||||
ensure_exists=ensure_exists,
|
||||
).user_config_dir
|
||||
|
||||
|
||||
def site_config_dir(
|
||||
appname: str | None = None,
|
||||
appauthor: str | None | Literal[False] = None,
|
||||
version: str | None = None,
|
||||
multipath: bool = False, # noqa: FBT001, FBT002
|
||||
ensure_exists: bool = False, # noqa: FBT001, FBT002
|
||||
) -> str:
|
||||
"""
|
||||
:param appname: See `appname <platformdirs.api.PlatformDirsABC.appname>`.
|
||||
:param appauthor: See `appauthor <platformdirs.api.PlatformDirsABC.appauthor>`.
|
||||
:param version: See `version <platformdirs.api.PlatformDirsABC.version>`.
|
||||
:param multipath: See `roaming <platformdirs.api.PlatformDirsABC.multipath>`.
|
||||
:param ensure_exists: See `ensure_exists <platformdirs.api.PlatformDirsABC.ensure_exists>`.
|
||||
:returns: config directory shared by the users
|
||||
"""
|
||||
return PlatformDirs(
|
||||
appname=appname,
|
||||
appauthor=appauthor,
|
||||
version=version,
|
||||
multipath=multipath,
|
||||
ensure_exists=ensure_exists,
|
||||
).site_config_dir
|
||||
|
||||
|
||||
def user_cache_dir(
|
||||
appname: str | None = None,
|
||||
appauthor: str | None | Literal[False] = None,
|
||||
version: str | None = None,
|
||||
opinion: bool = True, # noqa: FBT001, FBT002
|
||||
ensure_exists: bool = False, # noqa: FBT001, FBT002
|
||||
) -> str:
|
||||
"""
|
||||
:param appname: See `appname <platformdirs.api.PlatformDirsABC.appname>`.
|
||||
:param appauthor: See `appauthor <platformdirs.api.PlatformDirsABC.appauthor>`.
|
||||
:param version: See `version <platformdirs.api.PlatformDirsABC.version>`.
|
||||
:param opinion: See `roaming <platformdirs.api.PlatformDirsABC.opinion>`.
|
||||
:param ensure_exists: See `ensure_exists <platformdirs.api.PlatformDirsABC.ensure_exists>`.
|
||||
:returns: cache directory tied to the user
|
||||
"""
|
||||
return PlatformDirs(
|
||||
appname=appname,
|
||||
appauthor=appauthor,
|
||||
version=version,
|
||||
opinion=opinion,
|
||||
ensure_exists=ensure_exists,
|
||||
).user_cache_dir
|
||||
|
||||
|
||||
def site_cache_dir(
|
||||
appname: str | None = None,
|
||||
appauthor: str | None | Literal[False] = None,
|
||||
version: str | None = None,
|
||||
opinion: bool = True, # noqa: FBT001, FBT002
|
||||
ensure_exists: bool = False, # noqa: FBT001, FBT002
|
||||
) -> str:
|
||||
"""
|
||||
:param appname: See `appname <platformdirs.api.PlatformDirsABC.appname>`.
|
||||
:param appauthor: See `appauthor <platformdirs.api.PlatformDirsABC.appauthor>`.
|
||||
:param version: See `version <platformdirs.api.PlatformDirsABC.version>`.
|
||||
:param opinion: See `opinion <platformdirs.api.PlatformDirsABC.opinion>`.
|
||||
:param ensure_exists: See `ensure_exists <platformdirs.api.PlatformDirsABC.ensure_exists>`.
|
||||
:returns: cache directory tied to the user
|
||||
"""
|
||||
return PlatformDirs(
|
||||
appname=appname,
|
||||
appauthor=appauthor,
|
||||
version=version,
|
||||
opinion=opinion,
|
||||
ensure_exists=ensure_exists,
|
||||
).site_cache_dir
|
||||
|
||||
|
||||
def user_state_dir(
|
||||
appname: str | None = None,
|
||||
appauthor: str | None | Literal[False] = None,
|
||||
version: str | None = None,
|
||||
roaming: bool = False, # noqa: FBT001, FBT002
|
||||
ensure_exists: bool = False, # noqa: FBT001, FBT002
|
||||
) -> str:
|
||||
"""
|
||||
:param appname: See `appname <platformdirs.api.PlatformDirsABC.appname>`.
|
||||
:param appauthor: See `appauthor <platformdirs.api.PlatformDirsABC.appauthor>`.
|
||||
:param version: See `version <platformdirs.api.PlatformDirsABC.version>`.
|
||||
:param roaming: See `roaming <platformdirs.api.PlatformDirsABC.roaming>`.
|
||||
:param ensure_exists: See `ensure_exists <platformdirs.api.PlatformDirsABC.ensure_exists>`.
|
||||
:returns: state directory tied to the user
|
||||
"""
|
||||
return PlatformDirs(
|
||||
appname=appname,
|
||||
appauthor=appauthor,
|
||||
version=version,
|
||||
roaming=roaming,
|
||||
ensure_exists=ensure_exists,
|
||||
).user_state_dir
|
||||
|
||||
|
||||
def user_log_dir(
|
||||
appname: str | None = None,
|
||||
appauthor: str | None | Literal[False] = None,
|
||||
version: str | None = None,
|
||||
opinion: bool = True, # noqa: FBT001, FBT002
|
||||
ensure_exists: bool = False, # noqa: FBT001, FBT002
|
||||
) -> str:
|
||||
"""
|
||||
:param appname: See `appname <platformdirs.api.PlatformDirsABC.appname>`.
|
||||
:param appauthor: See `appauthor <platformdirs.api.PlatformDirsABC.appauthor>`.
|
||||
:param version: See `version <platformdirs.api.PlatformDirsABC.version>`.
|
||||
:param opinion: See `roaming <platformdirs.api.PlatformDirsABC.opinion>`.
|
||||
:param ensure_exists: See `ensure_exists <platformdirs.api.PlatformDirsABC.ensure_exists>`.
|
||||
:returns: log directory tied to the user
|
||||
"""
|
||||
return PlatformDirs(
|
||||
appname=appname,
|
||||
appauthor=appauthor,
|
||||
version=version,
|
||||
opinion=opinion,
|
||||
ensure_exists=ensure_exists,
|
||||
).user_log_dir
|
||||
|
||||
|
||||
def user_documents_dir() -> str:
|
||||
""":returns: documents directory tied to the user"""
|
||||
return PlatformDirs().user_documents_dir
|
||||
|
||||
|
||||
def user_downloads_dir() -> str:
|
||||
""":returns: downloads directory tied to the user"""
|
||||
return PlatformDirs().user_downloads_dir
|
||||
|
||||
|
||||
def user_pictures_dir() -> str:
|
||||
""":returns: pictures directory tied to the user"""
|
||||
return PlatformDirs().user_pictures_dir
|
||||
|
||||
|
||||
def user_videos_dir() -> str:
|
||||
""":returns: videos directory tied to the user"""
|
||||
return PlatformDirs().user_videos_dir
|
||||
|
||||
|
||||
def user_music_dir() -> str:
|
||||
""":returns: music directory tied to the user"""
|
||||
return PlatformDirs().user_music_dir
|
||||
|
||||
|
||||
def user_desktop_dir() -> str:
|
||||
""":returns: desktop directory tied to the user"""
|
||||
return PlatformDirs().user_desktop_dir
|
||||
|
||||
|
||||
def user_runtime_dir(
|
||||
appname: str | None = None,
|
||||
appauthor: str | None | Literal[False] = None,
|
||||
version: str | None = None,
|
||||
opinion: bool = True, # noqa: FBT001, FBT002
|
||||
ensure_exists: bool = False, # noqa: FBT001, FBT002
|
||||
) -> str:
|
||||
"""
|
||||
:param appname: See `appname <platformdirs.api.PlatformDirsABC.appname>`.
|
||||
:param appauthor: See `appauthor <platformdirs.api.PlatformDirsABC.appauthor>`.
|
||||
:param version: See `version <platformdirs.api.PlatformDirsABC.version>`.
|
||||
:param opinion: See `opinion <platformdirs.api.PlatformDirsABC.opinion>`.
|
||||
:param ensure_exists: See `ensure_exists <platformdirs.api.PlatformDirsABC.ensure_exists>`.
|
||||
:returns: runtime directory tied to the user
|
||||
"""
|
||||
return PlatformDirs(
|
||||
appname=appname,
|
||||
appauthor=appauthor,
|
||||
version=version,
|
||||
opinion=opinion,
|
||||
ensure_exists=ensure_exists,
|
||||
).user_runtime_dir
|
||||
|
||||
|
||||
def site_runtime_dir(
|
||||
appname: str | None = None,
|
||||
appauthor: str | None | Literal[False] = None,
|
||||
version: str | None = None,
|
||||
opinion: bool = True, # noqa: FBT001, FBT002
|
||||
ensure_exists: bool = False, # noqa: FBT001, FBT002
|
||||
) -> str:
|
||||
"""
|
||||
:param appname: See `appname <platformdirs.api.PlatformDirsABC.appname>`.
|
||||
:param appauthor: See `appauthor <platformdirs.api.PlatformDirsABC.appauthor>`.
|
||||
:param version: See `version <platformdirs.api.PlatformDirsABC.version>`.
|
||||
:param opinion: See `opinion <platformdirs.api.PlatformDirsABC.opinion>`.
|
||||
:param ensure_exists: See `ensure_exists <platformdirs.api.PlatformDirsABC.ensure_exists>`.
|
||||
:returns: runtime directory shared by users
|
||||
"""
|
||||
return PlatformDirs(
|
||||
appname=appname,
|
||||
appauthor=appauthor,
|
||||
version=version,
|
||||
opinion=opinion,
|
||||
ensure_exists=ensure_exists,
|
||||
).site_runtime_dir
|
||||
|
||||
|
||||
def user_data_path(
|
||||
appname: str | None = None,
|
||||
appauthor: str | None | Literal[False] = None,
|
||||
version: str | None = None,
|
||||
roaming: bool = False, # noqa: FBT001, FBT002
|
||||
ensure_exists: bool = False, # noqa: FBT001, FBT002
|
||||
) -> Path:
|
||||
"""
|
||||
:param appname: See `appname <platformdirs.api.PlatformDirsABC.appname>`.
|
||||
:param appauthor: See `appauthor <platformdirs.api.PlatformDirsABC.appauthor>`.
|
||||
:param version: See `version <platformdirs.api.PlatformDirsABC.version>`.
|
||||
:param roaming: See `roaming <platformdirs.api.PlatformDirsABC.roaming>`.
|
||||
:param ensure_exists: See `ensure_exists <platformdirs.api.PlatformDirsABC.ensure_exists>`.
|
||||
:returns: data path tied to the user
|
||||
"""
|
||||
return PlatformDirs(
|
||||
appname=appname,
|
||||
appauthor=appauthor,
|
||||
version=version,
|
||||
roaming=roaming,
|
||||
ensure_exists=ensure_exists,
|
||||
).user_data_path
|
||||
|
||||
|
||||
def site_data_path(
|
||||
appname: str | None = None,
|
||||
appauthor: str | None | Literal[False] = None,
|
||||
version: str | None = None,
|
||||
multipath: bool = False, # noqa: FBT001, FBT002
|
||||
ensure_exists: bool = False, # noqa: FBT001, FBT002
|
||||
) -> Path:
|
||||
"""
|
||||
:param appname: See `appname <platformdirs.api.PlatformDirsABC.appname>`.
|
||||
:param appauthor: See `appauthor <platformdirs.api.PlatformDirsABC.appauthor>`.
|
||||
:param version: See `version <platformdirs.api.PlatformDirsABC.version>`.
|
||||
:param multipath: See `multipath <platformdirs.api.PlatformDirsABC.multipath>`.
|
||||
:param ensure_exists: See `ensure_exists <platformdirs.api.PlatformDirsABC.ensure_exists>`.
|
||||
:returns: data path shared by users
|
||||
"""
|
||||
return PlatformDirs(
|
||||
appname=appname,
|
||||
appauthor=appauthor,
|
||||
version=version,
|
||||
multipath=multipath,
|
||||
ensure_exists=ensure_exists,
|
||||
).site_data_path
|
||||
|
||||
|
||||
def user_config_path(
|
||||
appname: str | None = None,
|
||||
appauthor: str | None | Literal[False] = None,
|
||||
version: str | None = None,
|
||||
roaming: bool = False, # noqa: FBT001, FBT002
|
||||
ensure_exists: bool = False, # noqa: FBT001, FBT002
|
||||
) -> Path:
|
||||
"""
|
||||
:param appname: See `appname <platformdirs.api.PlatformDirsABC.appname>`.
|
||||
:param appauthor: See `appauthor <platformdirs.api.PlatformDirsABC.appauthor>`.
|
||||
:param version: See `version <platformdirs.api.PlatformDirsABC.version>`.
|
||||
:param roaming: See `roaming <platformdirs.api.PlatformDirsABC.roaming>`.
|
||||
:param ensure_exists: See `ensure_exists <platformdirs.api.PlatformDirsABC.ensure_exists>`.
|
||||
:returns: config path tied to the user
|
||||
"""
|
||||
return PlatformDirs(
|
||||
appname=appname,
|
||||
appauthor=appauthor,
|
||||
version=version,
|
||||
roaming=roaming,
|
||||
ensure_exists=ensure_exists,
|
||||
).user_config_path
|
||||
|
||||
|
||||
def site_config_path(
|
||||
appname: str | None = None,
|
||||
appauthor: str | None | Literal[False] = None,
|
||||
version: str | None = None,
|
||||
multipath: bool = False, # noqa: FBT001, FBT002
|
||||
ensure_exists: bool = False, # noqa: FBT001, FBT002
|
||||
) -> Path:
|
||||
"""
|
||||
:param appname: See `appname <platformdirs.api.PlatformDirsABC.appname>`.
|
||||
:param appauthor: See `appauthor <platformdirs.api.PlatformDirsABC.appauthor>`.
|
||||
:param version: See `version <platformdirs.api.PlatformDirsABC.version>`.
|
||||
:param multipath: See `roaming <platformdirs.api.PlatformDirsABC.multipath>`.
|
||||
:param ensure_exists: See `ensure_exists <platformdirs.api.PlatformDirsABC.ensure_exists>`.
|
||||
:returns: config path shared by the users
|
||||
"""
|
||||
return PlatformDirs(
|
||||
appname=appname,
|
||||
appauthor=appauthor,
|
||||
version=version,
|
||||
multipath=multipath,
|
||||
ensure_exists=ensure_exists,
|
||||
).site_config_path
|
||||
|
||||
|
||||
def site_cache_path(
|
||||
appname: str | None = None,
|
||||
appauthor: str | None | Literal[False] = None,
|
||||
version: str | None = None,
|
||||
opinion: bool = True, # noqa: FBT001, FBT002
|
||||
ensure_exists: bool = False, # noqa: FBT001, FBT002
|
||||
) -> Path:
|
||||
"""
|
||||
:param appname: See `appname <platformdirs.api.PlatformDirsABC.appname>`.
|
||||
:param appauthor: See `appauthor <platformdirs.api.PlatformDirsABC.appauthor>`.
|
||||
:param version: See `version <platformdirs.api.PlatformDirsABC.version>`.
|
||||
:param opinion: See `opinion <platformdirs.api.PlatformDirsABC.opinion>`.
|
||||
:param ensure_exists: See `ensure_exists <platformdirs.api.PlatformDirsABC.ensure_exists>`.
|
||||
:returns: cache directory tied to the user
|
||||
"""
|
||||
return PlatformDirs(
|
||||
appname=appname,
|
||||
appauthor=appauthor,
|
||||
version=version,
|
||||
opinion=opinion,
|
||||
ensure_exists=ensure_exists,
|
||||
).site_cache_path
|
||||
|
||||
|
||||
def user_cache_path(
|
||||
appname: str | None = None,
|
||||
appauthor: str | None | Literal[False] = None,
|
||||
version: str | None = None,
|
||||
opinion: bool = True, # noqa: FBT001, FBT002
|
||||
ensure_exists: bool = False, # noqa: FBT001, FBT002
|
||||
) -> Path:
|
||||
"""
|
||||
:param appname: See `appname <platformdirs.api.PlatformDirsABC.appname>`.
|
||||
:param appauthor: See `appauthor <platformdirs.api.PlatformDirsABC.appauthor>`.
|
||||
:param version: See `version <platformdirs.api.PlatformDirsABC.version>`.
|
||||
:param opinion: See `roaming <platformdirs.api.PlatformDirsABC.opinion>`.
|
||||
:param ensure_exists: See `ensure_exists <platformdirs.api.PlatformDirsABC.ensure_exists>`.
|
||||
:returns: cache path tied to the user
|
||||
"""
|
||||
return PlatformDirs(
|
||||
appname=appname,
|
||||
appauthor=appauthor,
|
||||
version=version,
|
||||
opinion=opinion,
|
||||
ensure_exists=ensure_exists,
|
||||
).user_cache_path
|
||||
|
||||
|
||||
def user_state_path(
|
||||
appname: str | None = None,
|
||||
appauthor: str | None | Literal[False] = None,
|
||||
version: str | None = None,
|
||||
roaming: bool = False, # noqa: FBT001, FBT002
|
||||
ensure_exists: bool = False, # noqa: FBT001, FBT002
|
||||
) -> Path:
|
||||
"""
|
||||
:param appname: See `appname <platformdirs.api.PlatformDirsABC.appname>`.
|
||||
:param appauthor: See `appauthor <platformdirs.api.PlatformDirsABC.appauthor>`.
|
||||
:param version: See `version <platformdirs.api.PlatformDirsABC.version>`.
|
||||
:param roaming: See `roaming <platformdirs.api.PlatformDirsABC.roaming>`.
|
||||
:param ensure_exists: See `ensure_exists <platformdirs.api.PlatformDirsABC.ensure_exists>`.
|
||||
:returns: state path tied to the user
|
||||
"""
|
||||
return PlatformDirs(
|
||||
appname=appname,
|
||||
appauthor=appauthor,
|
||||
version=version,
|
||||
roaming=roaming,
|
||||
ensure_exists=ensure_exists,
|
||||
).user_state_path
|
||||
|
||||
|
||||
def user_log_path(
|
||||
appname: str | None = None,
|
||||
appauthor: str | None | Literal[False] = None,
|
||||
version: str | None = None,
|
||||
opinion: bool = True, # noqa: FBT001, FBT002
|
||||
ensure_exists: bool = False, # noqa: FBT001, FBT002
|
||||
) -> Path:
|
||||
"""
|
||||
:param appname: See `appname <platformdirs.api.PlatformDirsABC.appname>`.
|
||||
:param appauthor: See `appauthor <platformdirs.api.PlatformDirsABC.appauthor>`.
|
||||
:param version: See `version <platformdirs.api.PlatformDirsABC.version>`.
|
||||
:param opinion: See `roaming <platformdirs.api.PlatformDirsABC.opinion>`.
|
||||
:param ensure_exists: See `ensure_exists <platformdirs.api.PlatformDirsABC.ensure_exists>`.
|
||||
:returns: log path tied to the user
|
||||
"""
|
||||
return PlatformDirs(
|
||||
appname=appname,
|
||||
appauthor=appauthor,
|
||||
version=version,
|
||||
opinion=opinion,
|
||||
ensure_exists=ensure_exists,
|
||||
).user_log_path
|
||||
|
||||
|
||||
def user_documents_path() -> Path:
|
||||
""":returns: documents path tied to the user"""
|
||||
return PlatformDirs().user_documents_path
|
||||
|
||||
|
||||
def user_downloads_path() -> Path:
|
||||
""":returns: downloads path tied to the user"""
|
||||
return PlatformDirs().user_downloads_path
|
||||
|
||||
|
||||
def user_pictures_path() -> Path:
|
||||
""":returns: pictures path tied to the user"""
|
||||
return PlatformDirs().user_pictures_path
|
||||
|
||||
|
||||
def user_videos_path() -> Path:
|
||||
""":returns: videos path tied to the user"""
|
||||
return PlatformDirs().user_videos_path
|
||||
|
||||
|
||||
def user_music_path() -> Path:
|
||||
""":returns: music path tied to the user"""
|
||||
return PlatformDirs().user_music_path
|
||||
|
||||
|
||||
def user_desktop_path() -> Path:
|
||||
""":returns: desktop path tied to the user"""
|
||||
return PlatformDirs().user_desktop_path
|
||||
|
||||
|
||||
def user_runtime_path(
|
||||
appname: str | None = None,
|
||||
appauthor: str | None | Literal[False] = None,
|
||||
version: str | None = None,
|
||||
opinion: bool = True, # noqa: FBT001, FBT002
|
||||
ensure_exists: bool = False, # noqa: FBT001, FBT002
|
||||
) -> Path:
|
||||
"""
|
||||
:param appname: See `appname <platformdirs.api.PlatformDirsABC.appname>`.
|
||||
:param appauthor: See `appauthor <platformdirs.api.PlatformDirsABC.appauthor>`.
|
||||
:param version: See `version <platformdirs.api.PlatformDirsABC.version>`.
|
||||
:param opinion: See `opinion <platformdirs.api.PlatformDirsABC.opinion>`.
|
||||
:param ensure_exists: See `ensure_exists <platformdirs.api.PlatformDirsABC.ensure_exists>`.
|
||||
:returns: runtime path tied to the user
|
||||
"""
|
||||
return PlatformDirs(
|
||||
appname=appname,
|
||||
appauthor=appauthor,
|
||||
version=version,
|
||||
opinion=opinion,
|
||||
ensure_exists=ensure_exists,
|
||||
).user_runtime_path
|
||||
|
||||
|
||||
def site_runtime_path(
|
||||
appname: str | None = None,
|
||||
appauthor: str | None | Literal[False] = None,
|
||||
version: str | None = None,
|
||||
opinion: bool = True, # noqa: FBT001, FBT002
|
||||
ensure_exists: bool = False, # noqa: FBT001, FBT002
|
||||
) -> Path:
|
||||
"""
|
||||
:param appname: See `appname <platformdirs.api.PlatformDirsABC.appname>`.
|
||||
:param appauthor: See `appauthor <platformdirs.api.PlatformDirsABC.appauthor>`.
|
||||
:param version: See `version <platformdirs.api.PlatformDirsABC.version>`.
|
||||
:param opinion: See `opinion <platformdirs.api.PlatformDirsABC.opinion>`.
|
||||
:param ensure_exists: See `ensure_exists <platformdirs.api.PlatformDirsABC.ensure_exists>`.
|
||||
:returns: runtime path shared by users
|
||||
"""
|
||||
return PlatformDirs(
|
||||
appname=appname,
|
||||
appauthor=appauthor,
|
||||
version=version,
|
||||
opinion=opinion,
|
||||
ensure_exists=ensure_exists,
|
||||
).site_runtime_path
|
||||
|
||||
|
||||
__all__ = [
|
||||
"__version__",
|
||||
"__version_info__",
|
||||
"PlatformDirs",
|
||||
"AppDirs",
|
||||
"PlatformDirsABC",
|
||||
"user_data_dir",
|
||||
"user_config_dir",
|
||||
"user_cache_dir",
|
||||
"user_state_dir",
|
||||
"user_log_dir",
|
||||
"user_documents_dir",
|
||||
"user_downloads_dir",
|
||||
"user_pictures_dir",
|
||||
"user_videos_dir",
|
||||
"user_music_dir",
|
||||
"user_desktop_dir",
|
||||
"user_runtime_dir",
|
||||
"site_data_dir",
|
||||
"site_config_dir",
|
||||
"site_cache_dir",
|
||||
"site_runtime_dir",
|
||||
"user_data_path",
|
||||
"user_config_path",
|
||||
"user_cache_path",
|
||||
"user_state_path",
|
||||
"user_log_path",
|
||||
"user_documents_path",
|
||||
"user_downloads_path",
|
||||
"user_pictures_path",
|
||||
"user_videos_path",
|
||||
"user_music_path",
|
||||
"user_desktop_path",
|
||||
"user_runtime_path",
|
||||
"site_data_path",
|
||||
"site_config_path",
|
||||
"site_cache_path",
|
||||
"site_runtime_path",
|
||||
]
|
54
lib/platformdirs/__main__.py
Normal file
54
lib/platformdirs/__main__.py
Normal file
|
@ -0,0 +1,54 @@
|
|||
"""Main entry point."""
|
||||
from __future__ import annotations
|
||||
|
||||
from platformdirs import PlatformDirs, __version__
|
||||
|
||||
PROPS = (
|
||||
"user_data_dir",
|
||||
"user_config_dir",
|
||||
"user_cache_dir",
|
||||
"user_state_dir",
|
||||
"user_log_dir",
|
||||
"user_documents_dir",
|
||||
"user_downloads_dir",
|
||||
"user_pictures_dir",
|
||||
"user_videos_dir",
|
||||
"user_music_dir",
|
||||
"user_runtime_dir",
|
||||
"site_data_dir",
|
||||
"site_config_dir",
|
||||
"site_cache_dir",
|
||||
"site_runtime_dir",
|
||||
)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Run main entry point."""
|
||||
app_name = "MyApp"
|
||||
app_author = "MyCompany"
|
||||
|
||||
print(f"-- platformdirs {__version__} --") # noqa: T201
|
||||
|
||||
print("-- app dirs (with optional 'version')") # noqa: T201
|
||||
dirs = PlatformDirs(app_name, app_author, version="1.0")
|
||||
for prop in PROPS:
|
||||
print(f"{prop}: {getattr(dirs, prop)}") # noqa: T201
|
||||
|
||||
print("\n-- app dirs (without optional 'version')") # noqa: T201
|
||||
dirs = PlatformDirs(app_name, app_author)
|
||||
for prop in PROPS:
|
||||
print(f"{prop}: {getattr(dirs, prop)}") # noqa: T201
|
||||
|
||||
print("\n-- app dirs (without optional 'appauthor')") # noqa: T201
|
||||
dirs = PlatformDirs(app_name)
|
||||
for prop in PROPS:
|
||||
print(f"{prop}: {getattr(dirs, prop)}") # noqa: T201
|
||||
|
||||
print("\n-- app dirs (with disabled 'appauthor')") # noqa: T201
|
||||
dirs = PlatformDirs(app_name, appauthor=False)
|
||||
for prop in PROPS:
|
||||
print(f"{prop}: {getattr(dirs, prop)}") # noqa: T201
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
220
lib/platformdirs/android.py
Normal file
220
lib/platformdirs/android.py
Normal file
|
@ -0,0 +1,220 @@
|
|||
"""Android."""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from functools import lru_cache
|
||||
from typing import cast
|
||||
|
||||
from .api import PlatformDirsABC
|
||||
|
||||
|
||||
class Android(PlatformDirsABC):
|
||||
"""
|
||||
Follows the guidance `from here <https://android.stackexchange.com/a/216132>`_. Makes use of the
|
||||
`appname <platformdirs.api.PlatformDirsABC.appname>`,
|
||||
`version <platformdirs.api.PlatformDirsABC.version>`,
|
||||
`ensure_exists <platformdirs.api.PlatformDirsABC.ensure_exists>`.
|
||||
"""
|
||||
|
||||
@property
|
||||
def user_data_dir(self) -> str:
|
||||
""":return: data directory tied to the user, e.g. ``/data/user/<userid>/<packagename>/files/<AppName>``"""
|
||||
return self._append_app_name_and_version(cast(str, _android_folder()), "files")
|
||||
|
||||
@property
|
||||
def site_data_dir(self) -> str:
|
||||
""":return: data directory shared by users, same as `user_data_dir`"""
|
||||
return self.user_data_dir
|
||||
|
||||
@property
|
||||
def user_config_dir(self) -> str:
|
||||
"""
|
||||
:return: config directory tied to the user, e.g. \
|
||||
``/data/user/<userid>/<packagename>/shared_prefs/<AppName>``
|
||||
"""
|
||||
return self._append_app_name_and_version(cast(str, _android_folder()), "shared_prefs")
|
||||
|
||||
@property
|
||||
def site_config_dir(self) -> str:
|
||||
""":return: config directory shared by the users, same as `user_config_dir`"""
|
||||
return self.user_config_dir
|
||||
|
||||
@property
|
||||
def user_cache_dir(self) -> str:
|
||||
""":return: cache directory tied to the user, e.g. e.g. ``/data/user/<userid>/<packagename>/cache/<AppName>``"""
|
||||
return self._append_app_name_and_version(cast(str, _android_folder()), "cache")
|
||||
|
||||
@property
|
||||
def site_cache_dir(self) -> str:
|
||||
""":return: cache directory shared by users, same as `user_cache_dir`"""
|
||||
return self.user_cache_dir
|
||||
|
||||
@property
|
||||
def user_state_dir(self) -> str:
|
||||
""":return: state directory tied to the user, same as `user_data_dir`"""
|
||||
return self.user_data_dir
|
||||
|
||||
@property
|
||||
def user_log_dir(self) -> str:
|
||||
"""
|
||||
:return: log directory tied to the user, same as `user_cache_dir` if not opinionated else ``log`` in it,
|
||||
e.g. ``/data/user/<userid>/<packagename>/cache/<AppName>/log``
|
||||
"""
|
||||
path = self.user_cache_dir
|
||||
if self.opinion:
|
||||
path = os.path.join(path, "log") # noqa: PTH118
|
||||
return path
|
||||
|
||||
@property
|
||||
def user_documents_dir(self) -> str:
|
||||
""":return: documents directory tied to the user e.g. ``/storage/emulated/0/Documents``"""
|
||||
return _android_documents_folder()
|
||||
|
||||
@property
|
||||
def user_downloads_dir(self) -> str:
|
||||
""":return: downloads directory tied to the user e.g. ``/storage/emulated/0/Downloads``"""
|
||||
return _android_downloads_folder()
|
||||
|
||||
@property
|
||||
def user_pictures_dir(self) -> str:
|
||||
""":return: pictures directory tied to the user e.g. ``/storage/emulated/0/Pictures``"""
|
||||
return _android_pictures_folder()
|
||||
|
||||
@property
|
||||
def user_videos_dir(self) -> str:
|
||||
""":return: videos directory tied to the user e.g. ``/storage/emulated/0/DCIM/Camera``"""
|
||||
return _android_videos_folder()
|
||||
|
||||
@property
|
||||
def user_music_dir(self) -> str:
|
||||
""":return: music directory tied to the user e.g. ``/storage/emulated/0/Music``"""
|
||||
return _android_music_folder()
|
||||
|
||||
@property
|
||||
def user_desktop_dir(self) -> str:
|
||||
""":return: desktop directory tied to the user e.g. ``/storage/emulated/0/Desktop``"""
|
||||
return "/storage/emulated/0/Desktop"
|
||||
|
||||
@property
|
||||
def user_runtime_dir(self) -> str:
|
||||
"""
|
||||
:return: runtime directory tied to the user, same as `user_cache_dir` if not opinionated else ``tmp`` in it,
|
||||
e.g. ``/data/user/<userid>/<packagename>/cache/<AppName>/tmp``
|
||||
"""
|
||||
path = self.user_cache_dir
|
||||
if self.opinion:
|
||||
path = os.path.join(path, "tmp") # noqa: PTH118
|
||||
return path
|
||||
|
||||
@property
|
||||
def site_runtime_dir(self) -> str:
|
||||
""":return: runtime directory shared by users, same as `user_runtime_dir`"""
|
||||
return self.user_runtime_dir
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def _android_folder() -> str | None:
|
||||
""":return: base folder for the Android OS or None if it cannot be found"""
|
||||
try:
|
||||
# First try to get path to android app via pyjnius
|
||||
from jnius import autoclass
|
||||
|
||||
context = autoclass("android.content.Context")
|
||||
result: str | None = context.getFilesDir().getParentFile().getAbsolutePath()
|
||||
except Exception: # noqa: BLE001
|
||||
# if fails find an android folder looking path on the sys.path
|
||||
pattern = re.compile(r"/data/(data|user/\d+)/(.+)/files")
|
||||
for path in sys.path:
|
||||
if pattern.match(path):
|
||||
result = path.split("/files")[0]
|
||||
break
|
||||
else:
|
||||
result = None
|
||||
return result
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def _android_documents_folder() -> str:
|
||||
""":return: documents folder for the Android OS"""
|
||||
# Get directories with pyjnius
|
||||
try:
|
||||
from jnius import autoclass
|
||||
|
||||
context = autoclass("android.content.Context")
|
||||
environment = autoclass("android.os.Environment")
|
||||
documents_dir: str = context.getExternalFilesDir(environment.DIRECTORY_DOCUMENTS).getAbsolutePath()
|
||||
except Exception: # noqa: BLE001
|
||||
documents_dir = "/storage/emulated/0/Documents"
|
||||
|
||||
return documents_dir
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def _android_downloads_folder() -> str:
|
||||
""":return: downloads folder for the Android OS"""
|
||||
# Get directories with pyjnius
|
||||
try:
|
||||
from jnius import autoclass
|
||||
|
||||
context = autoclass("android.content.Context")
|
||||
environment = autoclass("android.os.Environment")
|
||||
downloads_dir: str = context.getExternalFilesDir(environment.DIRECTORY_DOWNLOADS).getAbsolutePath()
|
||||
except Exception: # noqa: BLE001
|
||||
downloads_dir = "/storage/emulated/0/Downloads"
|
||||
|
||||
return downloads_dir
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def _android_pictures_folder() -> str:
|
||||
""":return: pictures folder for the Android OS"""
|
||||
# Get directories with pyjnius
|
||||
try:
|
||||
from jnius import autoclass
|
||||
|
||||
context = autoclass("android.content.Context")
|
||||
environment = autoclass("android.os.Environment")
|
||||
pictures_dir: str = context.getExternalFilesDir(environment.DIRECTORY_PICTURES).getAbsolutePath()
|
||||
except Exception: # noqa: BLE001
|
||||
pictures_dir = "/storage/emulated/0/Pictures"
|
||||
|
||||
return pictures_dir
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def _android_videos_folder() -> str:
|
||||
""":return: videos folder for the Android OS"""
|
||||
# Get directories with pyjnius
|
||||
try:
|
||||
from jnius import autoclass
|
||||
|
||||
context = autoclass("android.content.Context")
|
||||
environment = autoclass("android.os.Environment")
|
||||
videos_dir: str = context.getExternalFilesDir(environment.DIRECTORY_DCIM).getAbsolutePath()
|
||||
except Exception: # noqa: BLE001
|
||||
videos_dir = "/storage/emulated/0/DCIM/Camera"
|
||||
|
||||
return videos_dir
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def _android_music_folder() -> str:
|
||||
""":return: music folder for the Android OS"""
|
||||
# Get directories with pyjnius
|
||||
try:
|
||||
from jnius import autoclass
|
||||
|
||||
context = autoclass("android.content.Context")
|
||||
environment = autoclass("android.os.Environment")
|
||||
music_dir: str = context.getExternalFilesDir(environment.DIRECTORY_MUSIC).getAbsolutePath()
|
||||
except Exception: # noqa: BLE001
|
||||
music_dir = "/storage/emulated/0/Music"
|
||||
|
||||
return music_dir
|
||||
|
||||
|
||||
__all__ = [
|
||||
"Android",
|
||||
]
|
243
lib/platformdirs/api.py
Normal file
243
lib/platformdirs/api.py
Normal file
|
@ -0,0 +1,243 @@
|
|||
"""Base API."""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from abc import ABC, abstractmethod
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import sys
|
||||
|
||||
if sys.version_info >= (3, 8): # pragma: no cover (py38+)
|
||||
from typing import Literal
|
||||
else: # pragma: no cover (py38+)
|
||||
from typing_extensions import Literal
|
||||
|
||||
|
||||
class PlatformDirsABC(ABC):
|
||||
"""Abstract base class for platform directories."""
|
||||
|
||||
def __init__( # noqa: PLR0913
|
||||
self,
|
||||
appname: str | None = None,
|
||||
appauthor: str | None | Literal[False] = None,
|
||||
version: str | None = None,
|
||||
roaming: bool = False, # noqa: FBT001, FBT002
|
||||
multipath: bool = False, # noqa: FBT001, FBT002
|
||||
opinion: bool = True, # noqa: FBT001, FBT002
|
||||
ensure_exists: bool = False, # noqa: FBT001, FBT002
|
||||
) -> None:
|
||||
"""
|
||||
Create a new platform directory.
|
||||
|
||||
:param appname: See `appname`.
|
||||
:param appauthor: See `appauthor`.
|
||||
:param version: See `version`.
|
||||
:param roaming: See `roaming`.
|
||||
:param multipath: See `multipath`.
|
||||
:param opinion: See `opinion`.
|
||||
:param ensure_exists: See `ensure_exists`.
|
||||
"""
|
||||
self.appname = appname #: The name of application.
|
||||
self.appauthor = appauthor
|
||||
"""
|
||||
The name of the app author or distributing body for this application. Typically, it is the owning company name.
|
||||
Defaults to `appname`. You may pass ``False`` to disable it.
|
||||
"""
|
||||
self.version = version
|
||||
"""
|
||||
An optional version path element to append to the path. You might want to use this if you want multiple versions
|
||||
of your app to be able to run independently. If used, this would typically be ``<major>.<minor>``.
|
||||
"""
|
||||
self.roaming = roaming
|
||||
"""
|
||||
Whether to use the roaming appdata directory on Windows. That means that for users on a Windows network setup
|
||||
for roaming profiles, this user data will be synced on login (see
|
||||
`here <http://technet.microsoft.com/en-us/library/cc766489(WS.10).aspx>`_).
|
||||
"""
|
||||
self.multipath = multipath
|
||||
"""
|
||||
An optional parameter which indicates that the entire list of data dirs should be returned.
|
||||
By default, the first item would only be returned.
|
||||
"""
|
||||
self.opinion = opinion #: A flag to indicating to use opinionated values.
|
||||
self.ensure_exists = ensure_exists
|
||||
"""
|
||||
Optionally create the directory (and any missing parents) upon access if it does not exist.
|
||||
By default, no directories are created.
|
||||
"""
|
||||
|
||||
def _append_app_name_and_version(self, *base: str) -> str:
|
||||
params = list(base[1:])
|
||||
if self.appname:
|
||||
params.append(self.appname)
|
||||
if self.version:
|
||||
params.append(self.version)
|
||||
path = os.path.join(base[0], *params) # noqa: PTH118
|
||||
self._optionally_create_directory(path)
|
||||
return path
|
||||
|
||||
def _optionally_create_directory(self, path: str) -> None:
|
||||
if self.ensure_exists:
|
||||
Path(path).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def user_data_dir(self) -> str:
|
||||
""":return: data directory tied to the user"""
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def site_data_dir(self) -> str:
|
||||
""":return: data directory shared by users"""
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def user_config_dir(self) -> str:
|
||||
""":return: config directory tied to the user"""
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def site_config_dir(self) -> str:
|
||||
""":return: config directory shared by the users"""
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def user_cache_dir(self) -> str:
|
||||
""":return: cache directory tied to the user"""
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def site_cache_dir(self) -> str:
|
||||
""":return: cache directory shared by users"""
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def user_state_dir(self) -> str:
|
||||
""":return: state directory tied to the user"""
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def user_log_dir(self) -> str:
|
||||
""":return: log directory tied to the user"""
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def user_documents_dir(self) -> str:
|
||||
""":return: documents directory tied to the user"""
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def user_downloads_dir(self) -> str:
|
||||
""":return: downloads directory tied to the user"""
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def user_pictures_dir(self) -> str:
|
||||
""":return: pictures directory tied to the user"""
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def user_videos_dir(self) -> str:
|
||||
""":return: videos directory tied to the user"""
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def user_music_dir(self) -> str:
|
||||
""":return: music directory tied to the user"""
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def user_desktop_dir(self) -> str:
|
||||
""":return: desktop directory tied to the user"""
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def user_runtime_dir(self) -> str:
|
||||
""":return: runtime directory tied to the user"""
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def site_runtime_dir(self) -> str:
|
||||
""":return: runtime directory shared by users"""
|
||||
|
||||
@property
|
||||
def user_data_path(self) -> Path:
|
||||
""":return: data path tied to the user"""
|
||||
return Path(self.user_data_dir)
|
||||
|
||||
@property
|
||||
def site_data_path(self) -> Path:
|
||||
""":return: data path shared by users"""
|
||||
return Path(self.site_data_dir)
|
||||
|
||||
@property
|
||||
def user_config_path(self) -> Path:
|
||||
""":return: config path tied to the user"""
|
||||
return Path(self.user_config_dir)
|
||||
|
||||
@property
|
||||
def site_config_path(self) -> Path:
|
||||
""":return: config path shared by the users"""
|
||||
return Path(self.site_config_dir)
|
||||
|
||||
@property
|
||||
def user_cache_path(self) -> Path:
|
||||
""":return: cache path tied to the user"""
|
||||
return Path(self.user_cache_dir)
|
||||
|
||||
@property
|
||||
def site_cache_path(self) -> Path:
|
||||
""":return: cache path shared by users"""
|
||||
return Path(self.site_cache_dir)
|
||||
|
||||
@property
|
||||
def user_state_path(self) -> Path:
|
||||
""":return: state path tied to the user"""
|
||||
return Path(self.user_state_dir)
|
||||
|
||||
@property
|
||||
def user_log_path(self) -> Path:
|
||||
""":return: log path tied to the user"""
|
||||
return Path(self.user_log_dir)
|
||||
|
||||
@property
|
||||
def user_documents_path(self) -> Path:
|
||||
""":return: documents path tied to the user"""
|
||||
return Path(self.user_documents_dir)
|
||||
|
||||
@property
|
||||
def user_downloads_path(self) -> Path:
|
||||
""":return: downloads path tied to the user"""
|
||||
return Path(self.user_downloads_dir)
|
||||
|
||||
@property
|
||||
def user_pictures_path(self) -> Path:
|
||||
""":return: pictures path tied to the user"""
|
||||
return Path(self.user_pictures_dir)
|
||||
|
||||
@property
|
||||
def user_videos_path(self) -> Path:
|
||||
""":return: videos path tied to the user"""
|
||||
return Path(self.user_videos_dir)
|
||||
|
||||
@property
|
||||
def user_music_path(self) -> Path:
|
||||
""":return: music path tied to the user"""
|
||||
return Path(self.user_music_dir)
|
||||
|
||||
@property
|
||||
def user_desktop_path(self) -> Path:
|
||||
""":return: desktop path tied to the user"""
|
||||
return Path(self.user_desktop_dir)
|
||||
|
||||
@property
|
||||
def user_runtime_path(self) -> Path:
|
||||
""":return: runtime path tied to the user"""
|
||||
return Path(self.user_runtime_dir)
|
||||
|
||||
@property
|
||||
def site_runtime_path(self) -> Path:
|
||||
""":return: runtime path shared by users"""
|
||||
return Path(self.site_runtime_dir)
|
126
lib/platformdirs/macos.py
Normal file
126
lib/platformdirs/macos.py
Normal file
|
@ -0,0 +1,126 @@
|
|||
"""macOS."""
|
||||
from __future__ import annotations
|
||||
|
||||
import os.path
|
||||
import sys
|
||||
|
||||
from .api import PlatformDirsABC
|
||||
|
||||
|
||||
class MacOS(PlatformDirsABC):
|
||||
"""
|
||||
Platform directories for the macOS operating system. Follows the guidance from `Apple documentation
|
||||
<https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/MacOSXDirectories/MacOSXDirectories.html>`_.
|
||||
Makes use of the `appname <platformdirs.api.PlatformDirsABC.appname>`,
|
||||
`version <platformdirs.api.PlatformDirsABC.version>`,
|
||||
`ensure_exists <platformdirs.api.PlatformDirsABC.ensure_exists>`.
|
||||
"""
|
||||
|
||||
@property
|
||||
def user_data_dir(self) -> str:
|
||||
""":return: data directory tied to the user, e.g. ``~/Library/Application Support/$appname/$version``"""
|
||||
return self._append_app_name_and_version(os.path.expanduser("~/Library/Application Support")) # noqa: PTH111
|
||||
|
||||
@property
|
||||
def site_data_dir(self) -> str:
|
||||
"""
|
||||
:return: data directory shared by users, e.g. ``/Library/Application Support/$appname/$version``.
|
||||
If we're using a Python binary managed by `Homebrew <https://brew.sh>`_, the directory
|
||||
will be under the Homebrew prefix, e.g. ``/opt/homebrew/share/$appname/$version``.
|
||||
If `multipath <platformdirs.api.PlatformDirsABC.multipath>` is enabled and we're in Homebrew,
|
||||
the response is a multi-path string separated by ":", e.g.
|
||||
``/opt/homebrew/share/$appname/$version:/Library/Application Support/$appname/$version``
|
||||
"""
|
||||
is_homebrew = sys.prefix.startswith("/opt/homebrew")
|
||||
path_list = [self._append_app_name_and_version("/opt/homebrew/share")] if is_homebrew else []
|
||||
path_list.append(self._append_app_name_and_version("/Library/Application Support"))
|
||||
if self.multipath:
|
||||
return os.pathsep.join(path_list)
|
||||
return path_list[0]
|
||||
|
||||
@property
|
||||
def user_config_dir(self) -> str:
|
||||
""":return: config directory tied to the user, same as `user_data_dir`"""
|
||||
return self.user_data_dir
|
||||
|
||||
@property
|
||||
def site_config_dir(self) -> str:
|
||||
""":return: config directory shared by the users, same as `site_data_dir`"""
|
||||
return self.site_data_dir
|
||||
|
||||
@property
|
||||
def user_cache_dir(self) -> str:
|
||||
""":return: cache directory tied to the user, e.g. ``~/Library/Caches/$appname/$version``"""
|
||||
return self._append_app_name_and_version(os.path.expanduser("~/Library/Caches")) # noqa: PTH111
|
||||
|
||||
@property
|
||||
def site_cache_dir(self) -> str:
|
||||
"""
|
||||
:return: cache directory shared by users, e.g. ``/Library/Caches/$appname/$version``.
|
||||
If we're using a Python binary managed by `Homebrew <https://brew.sh>`_, the directory
|
||||
will be under the Homebrew prefix, e.g. ``/opt/homebrew/var/cache/$appname/$version``.
|
||||
If `multipath <platformdirs.api.PlatformDirsABC.multipath>` is enabled and we're in Homebrew,
|
||||
the response is a multi-path string separated by ":", e.g.
|
||||
``/opt/homebrew/var/cache/$appname/$version:/Library/Caches/$appname/$version``
|
||||
"""
|
||||
is_homebrew = sys.prefix.startswith("/opt/homebrew")
|
||||
path_list = [self._append_app_name_and_version("/opt/homebrew/var/cache")] if is_homebrew else []
|
||||
path_list.append(self._append_app_name_and_version("/Library/Caches"))
|
||||
if self.multipath:
|
||||
return os.pathsep.join(path_list)
|
||||
return path_list[0]
|
||||
|
||||
@property
|
||||
def user_state_dir(self) -> str:
|
||||
""":return: state directory tied to the user, same as `user_data_dir`"""
|
||||
return self.user_data_dir
|
||||
|
||||
@property
|
||||
def user_log_dir(self) -> str:
|
||||
""":return: log directory tied to the user, e.g. ``~/Library/Logs/$appname/$version``"""
|
||||
return self._append_app_name_and_version(os.path.expanduser("~/Library/Logs")) # noqa: PTH111
|
||||
|
||||
@property
|
||||
def user_documents_dir(self) -> str:
|
||||
""":return: documents directory tied to the user, e.g. ``~/Documents``"""
|
||||
return os.path.expanduser("~/Documents") # noqa: PTH111
|
||||
|
||||
@property
|
||||
def user_downloads_dir(self) -> str:
|
||||
""":return: downloads directory tied to the user, e.g. ``~/Downloads``"""
|
||||
return os.path.expanduser("~/Downloads") # noqa: PTH111
|
||||
|
||||
@property
|
||||
def user_pictures_dir(self) -> str:
|
||||
""":return: pictures directory tied to the user, e.g. ``~/Pictures``"""
|
||||
return os.path.expanduser("~/Pictures") # noqa: PTH111
|
||||
|
||||
@property
|
||||
def user_videos_dir(self) -> str:
|
||||
""":return: videos directory tied to the user, e.g. ``~/Movies``"""
|
||||
return os.path.expanduser("~/Movies") # noqa: PTH111
|
||||
|
||||
@property
|
||||
def user_music_dir(self) -> str:
|
||||
""":return: music directory tied to the user, e.g. ``~/Music``"""
|
||||
return os.path.expanduser("~/Music") # noqa: PTH111
|
||||
|
||||
@property
|
||||
def user_desktop_dir(self) -> str:
|
||||
""":return: desktop directory tied to the user, e.g. ``~/Desktop``"""
|
||||
return os.path.expanduser("~/Desktop") # noqa: PTH111
|
||||
|
||||
@property
|
||||
def user_runtime_dir(self) -> str:
|
||||
""":return: runtime directory tied to the user, e.g. ``~/Library/Caches/TemporaryItems/$appname/$version``"""
|
||||
return self._append_app_name_and_version(os.path.expanduser("~/Library/Caches/TemporaryItems")) # noqa: PTH111
|
||||
|
||||
@property
|
||||
def site_runtime_dir(self) -> str:
|
||||
""":return: runtime directory shared by users, same as `user_runtime_dir`"""
|
||||
return self.user_runtime_dir
|
||||
|
||||
|
||||
__all__ = [
|
||||
"MacOS",
|
||||
]
|
0
lib/platformdirs/py.typed
Normal file
0
lib/platformdirs/py.typed
Normal file
251
lib/platformdirs/unix.py
Normal file
251
lib/platformdirs/unix.py
Normal file
|
@ -0,0 +1,251 @@
|
|||
"""Unix."""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
from configparser import ConfigParser
|
||||
from pathlib import Path
|
||||
|
||||
from .api import PlatformDirsABC
|
||||
|
||||
if sys.platform == "win32":
|
||||
|
||||
def getuid() -> int:
|
||||
msg = "should only be used on Unix"
|
||||
raise RuntimeError(msg)
|
||||
|
||||
else:
|
||||
from os import getuid
|
||||
|
||||
|
||||
class Unix(PlatformDirsABC):
|
||||
"""
|
||||
On Unix/Linux, we follow the
|
||||
`XDG Basedir Spec <https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html>`_. The spec allows
|
||||
overriding directories with environment variables. The examples show are the default values, alongside the name of
|
||||
the environment variable that overrides them. Makes use of the
|
||||
`appname <platformdirs.api.PlatformDirsABC.appname>`,
|
||||
`version <platformdirs.api.PlatformDirsABC.version>`,
|
||||
`multipath <platformdirs.api.PlatformDirsABC.multipath>`,
|
||||
`opinion <platformdirs.api.PlatformDirsABC.opinion>`,
|
||||
`ensure_exists <platformdirs.api.PlatformDirsABC.ensure_exists>`.
|
||||
"""
|
||||
|
||||
@property
|
||||
def user_data_dir(self) -> str:
|
||||
"""
|
||||
:return: data directory tied to the user, e.g. ``~/.local/share/$appname/$version`` or
|
||||
``$XDG_DATA_HOME/$appname/$version``
|
||||
"""
|
||||
path = os.environ.get("XDG_DATA_HOME", "")
|
||||
if not path.strip():
|
||||
path = os.path.expanduser("~/.local/share") # noqa: PTH111
|
||||
return self._append_app_name_and_version(path)
|
||||
|
||||
@property
|
||||
def site_data_dir(self) -> str:
|
||||
"""
|
||||
:return: data directories shared by users (if `multipath <platformdirs.api.PlatformDirsABC.multipath>` is
|
||||
enabled and ``XDG_DATA_DIR`` is set and a multi path the response is also a multi path separated by the OS
|
||||
path separator), e.g. ``/usr/local/share/$appname/$version`` or ``/usr/share/$appname/$version``
|
||||
"""
|
||||
# XDG default for $XDG_DATA_DIRS; only first, if multipath is False
|
||||
path = os.environ.get("XDG_DATA_DIRS", "")
|
||||
if not path.strip():
|
||||
path = f"/usr/local/share{os.pathsep}/usr/share"
|
||||
return self._with_multi_path(path)
|
||||
|
||||
def _with_multi_path(self, path: str) -> str:
|
||||
path_list = path.split(os.pathsep)
|
||||
if not self.multipath:
|
||||
path_list = path_list[0:1]
|
||||
path_list = [self._append_app_name_and_version(os.path.expanduser(p)) for p in path_list] # noqa: PTH111
|
||||
return os.pathsep.join(path_list)
|
||||
|
||||
@property
|
||||
def user_config_dir(self) -> str:
|
||||
"""
|
||||
:return: config directory tied to the user, e.g. ``~/.config/$appname/$version`` or
|
||||
``$XDG_CONFIG_HOME/$appname/$version``
|
||||
"""
|
||||
path = os.environ.get("XDG_CONFIG_HOME", "")
|
||||
if not path.strip():
|
||||
path = os.path.expanduser("~/.config") # noqa: PTH111
|
||||
return self._append_app_name_and_version(path)
|
||||
|
||||
@property
|
||||
def site_config_dir(self) -> str:
|
||||
"""
|
||||
:return: config directories shared by users (if `multipath <platformdirs.api.PlatformDirsABC.multipath>`
|
||||
is enabled and ``XDG_DATA_DIR`` is set and a multi path the response is also a multi path separated by the OS
|
||||
path separator), e.g. ``/etc/xdg/$appname/$version``
|
||||
"""
|
||||
# XDG default for $XDG_CONFIG_DIRS only first, if multipath is False
|
||||
path = os.environ.get("XDG_CONFIG_DIRS", "")
|
||||
if not path.strip():
|
||||
path = "/etc/xdg"
|
||||
return self._with_multi_path(path)
|
||||
|
||||
@property
|
||||
def user_cache_dir(self) -> str:
|
||||
"""
|
||||
:return: cache directory tied to the user, e.g. ``~/.cache/$appname/$version`` or
|
||||
``~/$XDG_CACHE_HOME/$appname/$version``
|
||||
"""
|
||||
path = os.environ.get("XDG_CACHE_HOME", "")
|
||||
if not path.strip():
|
||||
path = os.path.expanduser("~/.cache") # noqa: PTH111
|
||||
return self._append_app_name_and_version(path)
|
||||
|
||||
@property
|
||||
def site_cache_dir(self) -> str:
|
||||
""":return: cache directory shared by users, e.g. ``/var/tmp/$appname/$version``"""
|
||||
return self._append_app_name_and_version("/var/tmp") # noqa: S108
|
||||
|
||||
@property
|
||||
def user_state_dir(self) -> str:
|
||||
"""
|
||||
:return: state directory tied to the user, e.g. ``~/.local/state/$appname/$version`` or
|
||||
``$XDG_STATE_HOME/$appname/$version``
|
||||
"""
|
||||
path = os.environ.get("XDG_STATE_HOME", "")
|
||||
if not path.strip():
|
||||
path = os.path.expanduser("~/.local/state") # noqa: PTH111
|
||||
return self._append_app_name_and_version(path)
|
||||
|
||||
@property
|
||||
def user_log_dir(self) -> str:
|
||||
""":return: log directory tied to the user, same as `user_state_dir` if not opinionated else ``log`` in it"""
|
||||
path = self.user_state_dir
|
||||
if self.opinion:
|
||||
path = os.path.join(path, "log") # noqa: PTH118
|
||||
self._optionally_create_directory(path)
|
||||
return path
|
||||
|
||||
@property
|
||||
def user_documents_dir(self) -> str:
|
||||
""":return: documents directory tied to the user, e.g. ``~/Documents``"""
|
||||
return _get_user_media_dir("XDG_DOCUMENTS_DIR", "~/Documents")
|
||||
|
||||
@property
|
||||
def user_downloads_dir(self) -> str:
|
||||
""":return: downloads directory tied to the user, e.g. ``~/Downloads``"""
|
||||
return _get_user_media_dir("XDG_DOWNLOAD_DIR", "~/Downloads")
|
||||
|
||||
@property
|
||||
def user_pictures_dir(self) -> str:
|
||||
""":return: pictures directory tied to the user, e.g. ``~/Pictures``"""
|
||||
return _get_user_media_dir("XDG_PICTURES_DIR", "~/Pictures")
|
||||
|
||||
@property
|
||||
def user_videos_dir(self) -> str:
|
||||
""":return: videos directory tied to the user, e.g. ``~/Videos``"""
|
||||
return _get_user_media_dir("XDG_VIDEOS_DIR", "~/Videos")
|
||||
|
||||
@property
|
||||
def user_music_dir(self) -> str:
|
||||
""":return: music directory tied to the user, e.g. ``~/Music``"""
|
||||
return _get_user_media_dir("XDG_MUSIC_DIR", "~/Music")
|
||||
|
||||
@property
|
||||
def user_desktop_dir(self) -> str:
|
||||
""":return: desktop directory tied to the user, e.g. ``~/Desktop``"""
|
||||
return _get_user_media_dir("XDG_DESKTOP_DIR", "~/Desktop")
|
||||
|
||||
@property
|
||||
def user_runtime_dir(self) -> str:
|
||||
"""
|
||||
:return: runtime directory tied to the user, e.g. ``/run/user/$(id -u)/$appname/$version`` or
|
||||
``$XDG_RUNTIME_DIR/$appname/$version``.
|
||||
|
||||
For FreeBSD/OpenBSD/NetBSD, it would return ``/var/run/user/$(id -u)/$appname/$version`` if
|
||||
exists, otherwise ``/tmp/runtime-$(id -u)/$appname/$version``, if``$XDG_RUNTIME_DIR``
|
||||
is not set.
|
||||
"""
|
||||
path = os.environ.get("XDG_RUNTIME_DIR", "")
|
||||
if not path.strip():
|
||||
if sys.platform.startswith(("freebsd", "openbsd", "netbsd")):
|
||||
path = f"/var/run/user/{getuid()}"
|
||||
if not Path(path).exists():
|
||||
path = f"/tmp/runtime-{getuid()}" # noqa: S108
|
||||
else:
|
||||
path = f"/run/user/{getuid()}"
|
||||
return self._append_app_name_and_version(path)
|
||||
|
||||
@property
|
||||
def site_runtime_dir(self) -> str:
|
||||
"""
|
||||
:return: runtime directory shared by users, e.g. ``/run/$appname/$version`` or \
|
||||
``$XDG_RUNTIME_DIR/$appname/$version``.
|
||||
|
||||
Note that this behaves almost exactly like `user_runtime_dir` if ``$XDG_RUNTIME_DIR`` is set, but will
|
||||
fall back to paths associated to the root user instead of a regular logged-in user if it's not set.
|
||||
|
||||
If you wish to ensure that a logged-in root user path is returned e.g. ``/run/user/0``, use `user_runtime_dir`
|
||||
instead.
|
||||
|
||||
For FreeBSD/OpenBSD/NetBSD, it would return ``/var/run/$appname/$version`` if ``$XDG_RUNTIME_DIR`` is not set.
|
||||
"""
|
||||
path = os.environ.get("XDG_RUNTIME_DIR", "")
|
||||
if not path.strip():
|
||||
if sys.platform.startswith(("freebsd", "openbsd", "netbsd")):
|
||||
path = "/var/run"
|
||||
else:
|
||||
path = "/run"
|
||||
return self._append_app_name_and_version(path)
|
||||
|
||||
@property
|
||||
def site_data_path(self) -> Path:
|
||||
""":return: data path shared by users. Only return first item, even if ``multipath`` is set to ``True``"""
|
||||
return self._first_item_as_path_if_multipath(self.site_data_dir)
|
||||
|
||||
@property
|
||||
def site_config_path(self) -> Path:
|
||||
""":return: config path shared by the users. Only return first item, even if ``multipath`` is set to ``True``"""
|
||||
return self._first_item_as_path_if_multipath(self.site_config_dir)
|
||||
|
||||
@property
|
||||
def site_cache_path(self) -> Path:
|
||||
""":return: cache path shared by users. Only return first item, even if ``multipath`` is set to ``True``"""
|
||||
return self._first_item_as_path_if_multipath(self.site_cache_dir)
|
||||
|
||||
def _first_item_as_path_if_multipath(self, directory: str) -> Path:
|
||||
if self.multipath:
|
||||
# If multipath is True, the first path is returned.
|
||||
directory = directory.split(os.pathsep)[0]
|
||||
return Path(directory)
|
||||
|
||||
|
||||
def _get_user_media_dir(env_var: str, fallback_tilde_path: str) -> str:
|
||||
media_dir = _get_user_dirs_folder(env_var)
|
||||
if media_dir is None:
|
||||
media_dir = os.environ.get(env_var, "").strip()
|
||||
if not media_dir:
|
||||
media_dir = os.path.expanduser(fallback_tilde_path) # noqa: PTH111
|
||||
|
||||
return media_dir
|
||||
|
||||
|
||||
def _get_user_dirs_folder(key: str) -> str | None:
|
||||
"""Return directory from user-dirs.dirs config file. See https://freedesktop.org/wiki/Software/xdg-user-dirs/."""
|
||||
user_dirs_config_path = Path(Unix().user_config_dir) / "user-dirs.dirs"
|
||||
if user_dirs_config_path.exists():
|
||||
parser = ConfigParser()
|
||||
|
||||
with user_dirs_config_path.open() as stream:
|
||||
# Add fake section header, so ConfigParser doesn't complain
|
||||
parser.read_string(f"[top]\n{stream.read()}")
|
||||
|
||||
if key not in parser["top"]:
|
||||
return None
|
||||
|
||||
path = parser["top"][key].strip('"')
|
||||
# Handle relative home paths
|
||||
return path.replace("$HOME", os.path.expanduser("~")) # noqa: PTH111
|
||||
|
||||
return None
|
||||
|
||||
|
||||
__all__ = [
|
||||
"Unix",
|
||||
]
|
16
lib/platformdirs/version.py
Normal file
16
lib/platformdirs/version.py
Normal file
|
@ -0,0 +1,16 @@
|
|||
# file generated by setuptools_scm
|
||||
# don't change, don't track in version control
|
||||
TYPE_CHECKING = False
|
||||
if TYPE_CHECKING:
|
||||
from typing import Tuple, Union
|
||||
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
||||
else:
|
||||
VERSION_TUPLE = object
|
||||
|
||||
version: str
|
||||
__version__: str
|
||||
__version_tuple__: VERSION_TUPLE
|
||||
version_tuple: VERSION_TUPLE
|
||||
|
||||
__version__ = version = '3.11.0'
|
||||
__version_tuple__ = version_tuple = (3, 11, 0)
|
266
lib/platformdirs/windows.py
Normal file
266
lib/platformdirs/windows.py
Normal file
|
@ -0,0 +1,266 @@
|
|||
"""Windows."""
|
||||
from __future__ import annotations
|
||||
|
||||
import ctypes
|
||||
import os
|
||||
import sys
|
||||
from functools import lru_cache
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from .api import PlatformDirsABC
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable
|
||||
|
||||
|
||||
class Windows(PlatformDirsABC):
|
||||
"""
|
||||
`MSDN on where to store app data files
|
||||
<http://support.microsoft.com/default.aspx?scid=kb;en-us;310294#XSLTH3194121123120121120120>`_.
|
||||
Makes use of the
|
||||
`appname <platformdirs.api.PlatformDirsABC.appname>`,
|
||||
`appauthor <platformdirs.api.PlatformDirsABC.appauthor>`,
|
||||
`version <platformdirs.api.PlatformDirsABC.version>`,
|
||||
`roaming <platformdirs.api.PlatformDirsABC.roaming>`,
|
||||
`opinion <platformdirs.api.PlatformDirsABC.opinion>`,
|
||||
`ensure_exists <platformdirs.api.PlatformDirsABC.ensure_exists>`.
|
||||
"""
|
||||
|
||||
@property
|
||||
def user_data_dir(self) -> str:
|
||||
"""
|
||||
:return: data directory tied to the user, e.g.
|
||||
``%USERPROFILE%\\AppData\\Local\\$appauthor\\$appname`` (not roaming) or
|
||||
``%USERPROFILE%\\AppData\\Roaming\\$appauthor\\$appname`` (roaming)
|
||||
"""
|
||||
const = "CSIDL_APPDATA" if self.roaming else "CSIDL_LOCAL_APPDATA"
|
||||
path = os.path.normpath(get_win_folder(const))
|
||||
return self._append_parts(path)
|
||||
|
||||
def _append_parts(self, path: str, *, opinion_value: str | None = None) -> str:
|
||||
params = []
|
||||
if self.appname:
|
||||
if self.appauthor is not False:
|
||||
author = self.appauthor or self.appname
|
||||
params.append(author)
|
||||
params.append(self.appname)
|
||||
if opinion_value is not None and self.opinion:
|
||||
params.append(opinion_value)
|
||||
if self.version:
|
||||
params.append(self.version)
|
||||
path = os.path.join(path, *params) # noqa: PTH118
|
||||
self._optionally_create_directory(path)
|
||||
return path
|
||||
|
||||
@property
|
||||
def site_data_dir(self) -> str:
|
||||
""":return: data directory shared by users, e.g. ``C:\\ProgramData\\$appauthor\\$appname``"""
|
||||
path = os.path.normpath(get_win_folder("CSIDL_COMMON_APPDATA"))
|
||||
return self._append_parts(path)
|
||||
|
||||
@property
|
||||
def user_config_dir(self) -> str:
|
||||
""":return: config directory tied to the user, same as `user_data_dir`"""
|
||||
return self.user_data_dir
|
||||
|
||||
@property
|
||||
def site_config_dir(self) -> str:
|
||||
""":return: config directory shared by the users, same as `site_data_dir`"""
|
||||
return self.site_data_dir
|
||||
|
||||
@property
|
||||
def user_cache_dir(self) -> str:
|
||||
"""
|
||||
:return: cache directory tied to the user (if opinionated with ``Cache`` folder within ``$appname``) e.g.
|
||||
``%USERPROFILE%\\AppData\\Local\\$appauthor\\$appname\\Cache\\$version``
|
||||
"""
|
||||
path = os.path.normpath(get_win_folder("CSIDL_LOCAL_APPDATA"))
|
||||
return self._append_parts(path, opinion_value="Cache")
|
||||
|
||||
@property
|
||||
def site_cache_dir(self) -> str:
|
||||
""":return: cache directory shared by users, e.g. ``C:\\ProgramData\\$appauthor\\$appname\\Cache\\$version``"""
|
||||
path = os.path.normpath(get_win_folder("CSIDL_COMMON_APPDATA"))
|
||||
return self._append_parts(path, opinion_value="Cache")
|
||||
|
||||
@property
|
||||
def user_state_dir(self) -> str:
|
||||
""":return: state directory tied to the user, same as `user_data_dir`"""
|
||||
return self.user_data_dir
|
||||
|
||||
@property
|
||||
def user_log_dir(self) -> str:
|
||||
""":return: log directory tied to the user, same as `user_data_dir` if not opinionated else ``Logs`` in it"""
|
||||
path = self.user_data_dir
|
||||
if self.opinion:
|
||||
path = os.path.join(path, "Logs") # noqa: PTH118
|
||||
self._optionally_create_directory(path)
|
||||
return path
|
||||
|
||||
@property
|
||||
def user_documents_dir(self) -> str:
|
||||
""":return: documents directory tied to the user e.g. ``%USERPROFILE%\\Documents``"""
|
||||
return os.path.normpath(get_win_folder("CSIDL_PERSONAL"))
|
||||
|
||||
@property
|
||||
def user_downloads_dir(self) -> str:
|
||||
""":return: downloads directory tied to the user e.g. ``%USERPROFILE%\\Downloads``"""
|
||||
return os.path.normpath(get_win_folder("CSIDL_DOWNLOADS"))
|
||||
|
||||
@property
|
||||
def user_pictures_dir(self) -> str:
|
||||
""":return: pictures directory tied to the user e.g. ``%USERPROFILE%\\Pictures``"""
|
||||
return os.path.normpath(get_win_folder("CSIDL_MYPICTURES"))
|
||||
|
||||
@property
|
||||
def user_videos_dir(self) -> str:
|
||||
""":return: videos directory tied to the user e.g. ``%USERPROFILE%\\Videos``"""
|
||||
return os.path.normpath(get_win_folder("CSIDL_MYVIDEO"))
|
||||
|
||||
@property
|
||||
def user_music_dir(self) -> str:
|
||||
""":return: music directory tied to the user e.g. ``%USERPROFILE%\\Music``"""
|
||||
return os.path.normpath(get_win_folder("CSIDL_MYMUSIC"))
|
||||
|
||||
@property
|
||||
def user_desktop_dir(self) -> str:
|
||||
""":return: desktop directory tied to the user, e.g. ``%USERPROFILE%\\Desktop``"""
|
||||
return os.path.normpath(get_win_folder("CSIDL_DESKTOPDIRECTORY"))
|
||||
|
||||
@property
|
||||
def user_runtime_dir(self) -> str:
|
||||
"""
|
||||
:return: runtime directory tied to the user, e.g.
|
||||
``%USERPROFILE%\\AppData\\Local\\Temp\\$appauthor\\$appname``
|
||||
"""
|
||||
path = os.path.normpath(os.path.join(get_win_folder("CSIDL_LOCAL_APPDATA"), "Temp")) # noqa: PTH118
|
||||
return self._append_parts(path)
|
||||
|
||||
@property
|
||||
def site_runtime_dir(self) -> str:
|
||||
""":return: runtime directory shared by users, same as `user_runtime_dir`"""
|
||||
return self.user_runtime_dir
|
||||
|
||||
|
||||
def get_win_folder_from_env_vars(csidl_name: str) -> str:
|
||||
"""Get folder from environment variables."""
|
||||
result = get_win_folder_if_csidl_name_not_env_var(csidl_name)
|
||||
if result is not None:
|
||||
return result
|
||||
|
||||
env_var_name = {
|
||||
"CSIDL_APPDATA": "APPDATA",
|
||||
"CSIDL_COMMON_APPDATA": "ALLUSERSPROFILE",
|
||||
"CSIDL_LOCAL_APPDATA": "LOCALAPPDATA",
|
||||
}.get(csidl_name)
|
||||
if env_var_name is None:
|
||||
msg = f"Unknown CSIDL name: {csidl_name}"
|
||||
raise ValueError(msg)
|
||||
result = os.environ.get(env_var_name)
|
||||
if result is None:
|
||||
msg = f"Unset environment variable: {env_var_name}"
|
||||
raise ValueError(msg)
|
||||
return result
|
||||
|
||||
|
||||
def get_win_folder_if_csidl_name_not_env_var(csidl_name: str) -> str | None:
|
||||
"""Get folder for a CSIDL name that does not exist as an environment variable."""
|
||||
if csidl_name == "CSIDL_PERSONAL":
|
||||
return os.path.join(os.path.normpath(os.environ["USERPROFILE"]), "Documents") # noqa: PTH118
|
||||
|
||||
if csidl_name == "CSIDL_DOWNLOADS":
|
||||
return os.path.join(os.path.normpath(os.environ["USERPROFILE"]), "Downloads") # noqa: PTH118
|
||||
|
||||
if csidl_name == "CSIDL_MYPICTURES":
|
||||
return os.path.join(os.path.normpath(os.environ["USERPROFILE"]), "Pictures") # noqa: PTH118
|
||||
|
||||
if csidl_name == "CSIDL_MYVIDEO":
|
||||
return os.path.join(os.path.normpath(os.environ["USERPROFILE"]), "Videos") # noqa: PTH118
|
||||
|
||||
if csidl_name == "CSIDL_MYMUSIC":
|
||||
return os.path.join(os.path.normpath(os.environ["USERPROFILE"]), "Music") # noqa: PTH118
|
||||
return None
|
||||
|
||||
|
||||
def get_win_folder_from_registry(csidl_name: str) -> str:
|
||||
"""
|
||||
Get folder from the registry.
|
||||
|
||||
This is a fallback technique at best. I'm not sure if using the registry for these guarantees us the correct answer
|
||||
for all CSIDL_* names.
|
||||
"""
|
||||
shell_folder_name = {
|
||||
"CSIDL_APPDATA": "AppData",
|
||||
"CSIDL_COMMON_APPDATA": "Common AppData",
|
||||
"CSIDL_LOCAL_APPDATA": "Local AppData",
|
||||
"CSIDL_PERSONAL": "Personal",
|
||||
"CSIDL_DOWNLOADS": "{374DE290-123F-4565-9164-39C4925E467B}",
|
||||
"CSIDL_MYPICTURES": "My Pictures",
|
||||
"CSIDL_MYVIDEO": "My Video",
|
||||
"CSIDL_MYMUSIC": "My Music",
|
||||
}.get(csidl_name)
|
||||
if shell_folder_name is None:
|
||||
msg = f"Unknown CSIDL name: {csidl_name}"
|
||||
raise ValueError(msg)
|
||||
if sys.platform != "win32": # only needed for mypy type checker to know that this code runs only on Windows
|
||||
raise NotImplementedError
|
||||
import winreg
|
||||
|
||||
key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, r"Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders")
|
||||
directory, _ = winreg.QueryValueEx(key, shell_folder_name)
|
||||
return str(directory)
|
||||
|
||||
|
||||
def get_win_folder_via_ctypes(csidl_name: str) -> str:
|
||||
"""Get folder with ctypes."""
|
||||
# There is no 'CSIDL_DOWNLOADS'.
|
||||
# Use 'CSIDL_PROFILE' (40) and append the default folder 'Downloads' instead.
|
||||
# https://learn.microsoft.com/en-us/windows/win32/shell/knownfolderid
|
||||
|
||||
csidl_const = {
|
||||
"CSIDL_APPDATA": 26,
|
||||
"CSIDL_COMMON_APPDATA": 35,
|
||||
"CSIDL_LOCAL_APPDATA": 28,
|
||||
"CSIDL_PERSONAL": 5,
|
||||
"CSIDL_MYPICTURES": 39,
|
||||
"CSIDL_MYVIDEO": 14,
|
||||
"CSIDL_MYMUSIC": 13,
|
||||
"CSIDL_DOWNLOADS": 40,
|
||||
"CSIDL_DESKTOPDIRECTORY": 16,
|
||||
}.get(csidl_name)
|
||||
if csidl_const is None:
|
||||
msg = f"Unknown CSIDL name: {csidl_name}"
|
||||
raise ValueError(msg)
|
||||
|
||||
buf = ctypes.create_unicode_buffer(1024)
|
||||
windll = getattr(ctypes, "windll") # noqa: B009 # using getattr to avoid false positive with mypy type checker
|
||||
windll.shell32.SHGetFolderPathW(None, csidl_const, None, 0, buf)
|
||||
|
||||
# Downgrade to short path name if it has high-bit chars.
|
||||
if any(ord(c) > 255 for c in buf): # noqa: PLR2004
|
||||
buf2 = ctypes.create_unicode_buffer(1024)
|
||||
if windll.kernel32.GetShortPathNameW(buf.value, buf2, 1024):
|
||||
buf = buf2
|
||||
|
||||
if csidl_name == "CSIDL_DOWNLOADS":
|
||||
return os.path.join(buf.value, "Downloads") # noqa: PTH118
|
||||
|
||||
return buf.value
|
||||
|
||||
|
||||
def _pick_get_win_folder() -> Callable[[str], str]:
|
||||
if hasattr(ctypes, "windll"):
|
||||
return get_win_folder_via_ctypes
|
||||
try:
|
||||
import winreg # noqa: F401
|
||||
except ImportError:
|
||||
return get_win_folder_from_env_vars
|
||||
else:
|
||||
return get_win_folder_from_registry
|
||||
|
||||
|
||||
get_win_folder = lru_cache(maxsize=None)(_pick_get_win_folder())
|
||||
|
||||
__all__ = [
|
||||
"Windows",
|
||||
]
|
|
@ -543,13 +543,14 @@ NOTIFICATION_PARAMETERS = [
|
|||
{'name': 'Duration', 'type': 'int', 'value': 'duration', 'description': 'The duration (in minutes) for the item.'},
|
||||
{'name': 'Duration (sec)', 'type': 'int', 'value': 'duration_sec', 'description': 'The duration (in seconds) for the item.'},
|
||||
{'name': 'Duration (ms)', 'type': 'int', 'value': 'duration_ms', 'description': 'The duration (in milliseconds) for the item.'},
|
||||
{'name': 'Duration Time', 'type': 'str', 'value': 'duration_time', 'description': 'The duration (in time format) for the item.'},
|
||||
{'name': 'Poster URL', 'type': 'str', 'value': 'poster_url', 'description': 'A URL for the movie, TV show, or album poster.'},
|
||||
{'name': 'Plex ID', 'type': 'str', 'value': 'plex_id', 'description': 'The Plex ID for the item.', 'example': 'e.g. 5d7769a9594b2b001e6a6b7e'},
|
||||
{'name': 'Plex URL', 'type': 'str', 'value': 'plex_url', 'description': 'The Plex URL to your server for the item.'},
|
||||
{'name': 'IMDB ID', 'type': 'str', 'value': 'imdb_id', 'description': 'The IMDB ID for the movie.', 'example': 'e.g. tt2488496'},
|
||||
{'name': 'IMDB URL', 'type': 'str', 'value': 'imdb_url', 'description': 'The IMDB URL for the movie.'},
|
||||
{'name': 'TVDB ID', 'type': 'int', 'value': 'thetvdb_id', 'description': 'The TVDB ID for the TV show.', 'example': 'e.g. 121361'},
|
||||
{'name': 'TVDB URL', 'type': 'str', 'value': 'thetvdb_url', 'description': 'The TVDB URL for the TV show.'},
|
||||
{'name': 'IMDB ID', 'type': 'str', 'value': 'imdb_id', 'description': 'The IMDB ID for the movie or TV show.', 'example': 'e.g. tt2488496'},
|
||||
{'name': 'IMDB URL', 'type': 'str', 'value': 'imdb_url', 'description': 'The IMDB URL for the movie or TV show.'},
|
||||
{'name': 'TVDB ID', 'type': 'int', 'value': 'thetvdb_id', 'description': 'The TVDB ID for the movie or TV show.', 'example': 'e.g. 121361'},
|
||||
{'name': 'TVDB URL', 'type': 'str', 'value': 'thetvdb_url', 'description': 'The TVDB URL for the movie or TV show.'},
|
||||
{'name': 'TMDB ID', 'type': 'int', 'value': 'themoviedb_id', 'description': 'The TMDb ID for the movie or TV show.', 'example': 'e.g. 15260'},
|
||||
{'name': 'TMDB URL', 'type': 'str', 'value': 'themoviedb_url', 'description': 'The TMDb URL for the movie or TV show.'},
|
||||
{'name': 'TVmaze ID', 'type': 'int', 'value': 'tvmaze_id', 'description': 'The TVmaze ID for the TV show.', 'example': 'e.g. 290'},
|
||||
|
@ -599,7 +600,8 @@ NOTIFICATION_PARAMETERS = [
|
|||
{'name': 'Subtitle Language Code', 'type': 'str', 'value': 'subtitle_language_code', 'description': 'The subtitle language code of the original media.'},
|
||||
{'name': 'File', 'type': 'str', 'value': 'file', 'description': 'The file path to the item.'},
|
||||
{'name': 'Filename', 'type': 'str', 'value': 'filename', 'description': 'The file name of the item.'},
|
||||
{'name': 'File Size', 'type': 'int', 'value': 'file_size', 'description': 'The file size of the item.'},
|
||||
{'name': 'File Size', 'type': 'str', 'value': 'file_size', 'description': 'The file size of the item in human readable format.', 'example': '1.2 GB'},
|
||||
{'name': 'File Size Bytes', 'type': 'int', 'value': 'file_size_bytes', 'description': 'The file size of the item in bytes.'},
|
||||
{'name': 'Guid', 'type': 'str', 'value': 'guid', 'description': 'The full guid for the item.'},
|
||||
{'name': 'Section ID', 'type': 'int', 'value': 'section_id', 'description': 'The unique identifier for the library.'},
|
||||
{'name': 'Rating Key', 'type': 'int', 'value': 'rating_key', 'description': 'The unique identifier for the movie, episode, or track.'},
|
||||
|
@ -631,7 +633,7 @@ NOTIFICATION_PARAMETERS = [
|
|||
'parameters': [
|
||||
{'name': 'Update Version', 'type': 'str', 'value': 'update_version', 'description': 'The available update version for your Plex Server.'},
|
||||
{'name': 'Update Url', 'type': 'str', 'value': 'update_url', 'description': 'The download URL for the available update.'},
|
||||
{'name': 'Update Release Date', 'type': 'str', 'value': 'update_release_date', 'description': 'The release date of the available update.'},
|
||||
{'name': 'Update Release Date', 'type': 'str', 'value': 'update_release_date', 'description': 'The release date (in date format) of the available update.'},
|
||||
{'name': 'Update Channel', 'type': 'str', 'value': 'update_channel', 'description': 'The update channel.', 'example': 'Public or Plex Pass'},
|
||||
{'name': 'Update Platform', 'type': 'str', 'value': 'update_platform', 'description': 'The platform of your Plex Server.'},
|
||||
{'name': 'Update Distro', 'type': 'str', 'value': 'update_distro', 'description': 'The distro of your Plex Server.'},
|
||||
|
|
|
@ -24,7 +24,7 @@ from cloudinary.api import delete_resources_by_tag
|
|||
from cloudinary.uploader import upload
|
||||
from cloudinary.utils import cloudinary_url
|
||||
from collections import OrderedDict
|
||||
import datetime
|
||||
from datetime import date, datetime, timezone
|
||||
from functools import reduce, wraps
|
||||
import hashlib
|
||||
import imghdr
|
||||
|
@ -212,14 +212,14 @@ def timestamp():
|
|||
|
||||
|
||||
def today():
|
||||
today = datetime.date.today()
|
||||
yyyymmdd = datetime.date.isoformat(today)
|
||||
today = date.today()
|
||||
yyyymmdd = date.isoformat(today)
|
||||
|
||||
return yyyymmdd
|
||||
|
||||
|
||||
def utc_now_iso():
|
||||
utcnow = datetime.datetime.utcnow()
|
||||
utcnow = datetime.now(tz=timezone.utc).replace(tzinfo=None)
|
||||
|
||||
return utcnow.isoformat()
|
||||
|
||||
|
@ -236,7 +236,7 @@ def timestamp_to_YMDHMS(ts, sep=False):
|
|||
|
||||
|
||||
def timestamp_to_datetime(ts):
|
||||
return datetime.datetime.fromtimestamp(ts)
|
||||
return datetime.fromtimestamp(ts)
|
||||
|
||||
|
||||
def iso_to_YMD(iso):
|
||||
|
@ -248,7 +248,7 @@ def iso_to_datetime(iso):
|
|||
|
||||
|
||||
def datetime_to_iso(dt, to_date=False):
|
||||
if isinstance(dt, datetime.datetime):
|
||||
if isinstance(dt, datetime):
|
||||
if to_date:
|
||||
dt = dt.date()
|
||||
return dt.isoformat()
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import arrow
|
||||
import bleach
|
||||
|
@ -680,13 +681,14 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
|
|||
notify_params['trakt_url'] = 'https://trakt.tv/search/imdb/' + notify_params['imdb_id']
|
||||
|
||||
if 'thetvdb://' in notify_params['guid'] or notify_params['thetvdb_id']:
|
||||
thetvdb_media_type = 'movie' if notify_params['media_type'] == 'movie' else 'series'
|
||||
notify_params['thetvdb_id'] = notify_params['thetvdb_id'] or notify_params['guid'].split('thetvdb://')[1].split('/')[0].split('?')[0]
|
||||
notify_params['thetvdb_url'] = 'https://thetvdb.com/?tab=series&id=' + notify_params['thetvdb_id']
|
||||
notify_params['thetvdb_url'] = f'https://thetvdb.com/dereferrer/{thetvdb_media_type}/{notify_params["thetvdb_id"]}'
|
||||
notify_params['trakt_url'] = 'https://trakt.tv/search/tvdb/' + notify_params['thetvdb_id'] + '?type=show'
|
||||
|
||||
elif 'thetvdbdvdorder://' in notify_params['guid']:
|
||||
notify_params['thetvdb_id'] = notify_params['guid'].split('thetvdbdvdorder://')[1].split('/')[0].split('?')[0]
|
||||
notify_params['thetvdb_url'] = 'https://thetvdb.com/?tab=series&id=' + notify_params['thetvdb_id']
|
||||
notify_params['thetvdb_url'] = f'https://thetvdb.com/dereferrer/series/{notify_params["thetvdb_id"]}'
|
||||
notify_params['trakt_url'] = 'https://trakt.tv/search/tvdb/' + notify_params['thetvdb_id'] + '?type=show'
|
||||
|
||||
if 'themoviedb://' in notify_params['guid'] or notify_params['themoviedb_id']:
|
||||
|
@ -787,7 +789,7 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
|
|||
notify_params.update(tvmaze_info)
|
||||
|
||||
if tvmaze_info.get('thetvdb_id'):
|
||||
notify_params['thetvdb_url'] = 'https://thetvdb.com/?tab=series&id=' + str(tvmaze_info['thetvdb_id'])
|
||||
notify_params['thetvdb_url'] = f'https://thetvdb.com/dereferrer/series/{tvmaze_info["thetvdb_id"]}'
|
||||
notify_params['trakt_url'] = 'https://trakt.tv/search/tvdb/{}' + str(notify_params['thetvdb_id']) + '?type=show'
|
||||
if tvmaze_info.get('imdb_id'):
|
||||
notify_params['imdb_url'] = 'https://www.imdb.com/title/' + tvmaze_info['imdb_id']
|
||||
|
@ -963,8 +965,8 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
|
|||
'current_weekday': now_iso[2],
|
||||
'current_week': now_iso[1],
|
||||
'week_number': now_iso[1], # Keep for backwards compatibility
|
||||
'datestamp': now.format(date_format),
|
||||
'timestamp': now.format(time_format),
|
||||
'datestamp': CustomArrow(now, date_format),
|
||||
'timestamp': CustomArrow(now, time_format),
|
||||
'unixtime': helpers.timestamp(),
|
||||
'utctime': helpers.utc_now_iso(),
|
||||
# Stream parameters
|
||||
|
@ -988,21 +990,21 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
|
|||
'product': notify_params['product'],
|
||||
'player': notify_params['player'],
|
||||
'ip_address': notify_params.get('ip_address', 'N/A'),
|
||||
'started_datestamp': arrow.get(notify_params['started']).format(date_format) if notify_params['started'] else '',
|
||||
'started_timestamp': arrow.get(notify_params['started']).format(time_format) if notify_params['started'] else '',
|
||||
'started_datestamp': CustomArrow(arrow.get(notify_params['started']), date_format) if notify_params['started'] else '',
|
||||
'started_timestamp': CustomArrow(arrow.get(notify_params['started']), time_format) if notify_params['started'] else '',
|
||||
'started_unixtime': notify_params['started'],
|
||||
'stopped_datestamp': arrow.get(notify_params['stopped']).format(date_format) if notify_params['stopped'] else '',
|
||||
'stopped_timestamp': arrow.get(notify_params['stopped']).format(time_format) if notify_params['stopped'] else '',
|
||||
'stopped_datestamp': CustomArrow(arrow.get(notify_params['stopped']), date_format) if notify_params['stopped'] else '',
|
||||
'stopped_timestamp': CustomArrow(arrow.get(notify_params['stopped']), time_format) if notify_params['stopped'] else '',
|
||||
'stopped_unixtime': notify_params['stopped'],
|
||||
'stream_duration': stream_duration,
|
||||
'stream_duration_sec': stream_duration_sec,
|
||||
'stream_time': arrow.get(stream_duration_sec).format(duration_format),
|
||||
'stream_time': CustomArrow(arrow.get(stream_duration_sec), duration_format),
|
||||
'remaining_duration': remaining_duration,
|
||||
'remaining_duration_sec': remaining_duration_sec,
|
||||
'remaining_time': arrow.get(remaining_duration_sec).format(duration_format),
|
||||
'remaining_time': CustomArrow(arrow.get(remaining_duration_sec), duration_format),
|
||||
'progress_duration': progress_duration,
|
||||
'progress_duration_sec': progress_duration_sec,
|
||||
'progress_time': arrow.get(progress_duration_sec).format(duration_format),
|
||||
'progress_time': CustomArrow(arrow.get(progress_duration_sec), duration_format),
|
||||
'progress_percent': helpers.get_percent(progress_duration_sec, duration_sec),
|
||||
'view_offset': session.get('view_offset', 0),
|
||||
'initial_stream': notify_params['initial_stream'],
|
||||
|
@ -1110,15 +1112,15 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
|
|||
'track_count': grandchild_count,
|
||||
'year': notify_params['year'],
|
||||
'show_year': show_year,
|
||||
'release_date': arrow.get(notify_params['originally_available_at']).format(date_format)
|
||||
'release_date': CustomArrow(arrow.get(notify_params['originally_available_at']), date_format)
|
||||
if notify_params['originally_available_at'] else '',
|
||||
'air_date': arrow.get(notify_params['originally_available_at']).format(date_format)
|
||||
'air_date': CustomArrow(arrow.get(notify_params['originally_available_at']), date_format)
|
||||
if notify_params['originally_available_at'] else '',
|
||||
'added_date': arrow.get(int(notify_params['added_at'])).format(date_format)
|
||||
'added_date': CustomArrow(arrow.get(int(notify_params['added_at'])), date_format)
|
||||
if notify_params['added_at'] else '',
|
||||
'updated_date': arrow.get(int(notify_params['updated_at'])).format(date_format)
|
||||
'updated_date': CustomArrow(arrow.get(int(notify_params['updated_at'])), date_format)
|
||||
if notify_params['updated_at'] else '',
|
||||
'last_viewed_date': arrow.get(int(notify_params['last_viewed_at'])).format(date_format)
|
||||
'last_viewed_date': CustomArrow(arrow.get(int(notify_params['last_viewed_at'])), date_format)
|
||||
if notify_params['last_viewed_at'] else '',
|
||||
'studio': notify_params['studio'],
|
||||
'content_rating': notify_params['content_rating'],
|
||||
|
@ -1137,6 +1139,7 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
|
|||
'duration': duration,
|
||||
'duration_sec': duration_sec,
|
||||
'duration_ms': notify_params['duration'],
|
||||
'duration_time': CustomArrow(arrow.get(duration_sec), duration_format),
|
||||
'poster_title': notify_params['poster_title'],
|
||||
'poster_url': notify_params['poster_url'],
|
||||
'plex_id': notify_params['plex_id'],
|
||||
|
@ -1195,6 +1198,7 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
|
|||
'file': notify_params['file'],
|
||||
'filename': os.path.basename(notify_params['file'].replace('\\', os.sep)),
|
||||
'file_size': helpers.human_file_size(notify_params['file_size']),
|
||||
'file_size_bytes': notify_params['file_size'],
|
||||
'indexes': notify_params['indexes'],
|
||||
'guid': notify_params['guid'],
|
||||
'section_id': notify_params['section_id'],
|
||||
|
@ -1258,8 +1262,8 @@ def build_server_notify_params(notify_action=None, **kwargs):
|
|||
'current_weekday': now_iso[2],
|
||||
'current_week': now_iso[1],
|
||||
'week_number': now_iso[1], # Keep for backwards compatibility
|
||||
'datestamp': now.format(date_format),
|
||||
'timestamp': now.format(time_format),
|
||||
'datestamp': CustomArrow(now, date_format),
|
||||
'timestamp': CustomArrow(now, time_format),
|
||||
'unixtime': helpers.timestamp(),
|
||||
'utctime': helpers.utc_now_iso(),
|
||||
# Plex remote access parameters
|
||||
|
@ -1273,7 +1277,7 @@ def build_server_notify_params(notify_action=None, **kwargs):
|
|||
# Plex Media Server update parameters
|
||||
'update_version': pms_download_info['version'],
|
||||
'update_url': pms_download_info['download_url'],
|
||||
'update_release_date': arrow.get(pms_download_info['release_date']).format(date_format)
|
||||
'update_release_date': CustomArrow(arrow.get(pms_download_info['release_date']), date_format)
|
||||
if pms_download_info['release_date'] else '',
|
||||
'update_channel': 'Beta' if update_channel == 'beta' else 'Public',
|
||||
'update_platform': pms_download_info['platform'],
|
||||
|
@ -2069,3 +2073,21 @@ class CustomFormatter(Formatter):
|
|||
# result.append(self.format_field(obj, format_spec))
|
||||
|
||||
return ''.join(result), auto_arg_index
|
||||
|
||||
|
||||
class CustomArrow:
|
||||
def __init__(self, arrow_value: arrow.arrow.Arrow, default_format: Optional[str] = None):
|
||||
self.arrow_value = arrow_value
|
||||
self.default_format = default_format
|
||||
|
||||
def __format__(self, formatstr: str) -> str:
|
||||
if len(formatstr) > 0:
|
||||
return self.arrow_value.format(formatstr)
|
||||
|
||||
if self.default_format is not None:
|
||||
return self.__format__(self.default_format)
|
||||
|
||||
return str(self.arrow_value)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.__format__('')
|
||||
|
|
|
@ -794,6 +794,7 @@ class PrettyMetadata(object):
|
|||
'plexweb': 'Plex Web',
|
||||
'imdb': 'IMDB',
|
||||
'themoviedb': 'The Movie Database',
|
||||
'thetvdb': 'TheTVDB',
|
||||
'trakt': 'Trakt.tv'
|
||||
}
|
||||
|
||||
|
|
|
@ -17,4 +17,4 @@
|
|||
|
||||
|
||||
PLEXPY_BRANCH = "master"
|
||||
PLEXPY_RELEASE_VERSION = "v2.13.2"
|
||||
PLEXPY_RELEASE_VERSION = "v2.13.4"
|
|
@ -21,7 +21,7 @@
|
|||
# Session tool to be loaded.
|
||||
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from urllib.parse import quote, unquote
|
||||
|
||||
import cherrypy
|
||||
|
@ -370,7 +370,7 @@ class AuthController(object):
|
|||
|
||||
if valid_login:
|
||||
time_delta = timedelta(days=30) if remember_me == '1' else timedelta(minutes=60)
|
||||
expiry = datetime.utcnow() + time_delta
|
||||
expiry = datetime.now(tz=timezone.utc) + time_delta
|
||||
|
||||
payload = {
|
||||
'user_id': user_details['user_id'],
|
||||
|
@ -391,7 +391,7 @@ class AuthController(object):
|
|||
|
||||
jwt_cookie = str(JWT_COOKIE_NAME + plexpy.CONFIG.PMS_UUID)
|
||||
cherrypy.response.cookie[jwt_cookie] = jwt_token
|
||||
cherrypy.response.cookie[jwt_cookie]['expires'] = int(time_delta.total_seconds())
|
||||
cherrypy.response.cookie[jwt_cookie]['max-age'] = int(time_delta.total_seconds())
|
||||
cherrypy.response.cookie[jwt_cookie]['path'] = plexpy.HTTP_ROOT.rstrip('/') or '/'
|
||||
cherrypy.response.cookie[jwt_cookie]['httponly'] = True
|
||||
cherrypy.response.cookie[jwt_cookie]['samesite'] = 'lax'
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
appdirs==1.4.4
|
||||
apscheduler==3.10.1
|
||||
arrow==1.2.3
|
||||
beautifulsoup4==4.12.2
|
||||
|
@ -24,6 +23,7 @@ MarkupSafe==2.1.3
|
|||
musicbrainzngs==0.7.1
|
||||
packaging==23.1
|
||||
paho-mqtt==1.6.1
|
||||
platformdirs==3.11.0
|
||||
plexapi==4.15.4
|
||||
portend==3.2.0
|
||||
profilehooks==1.12.0
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue