[106] small updates

This commit is contained in:
meisnate12 2022-05-18 13:07:15 -04:00
parent 9d94f00ac7
commit 5369dc4fd8
18 changed files with 116 additions and 64 deletions

View file

@ -16,6 +16,4 @@ Please delete options that are not relevant.
## Checklist
- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my own code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] My code was submitted to the nightly branch of the repository.

View file

@ -8,7 +8,7 @@
[![Discord](https://img.shields.io/discord/822460010649878528?label=Discord&style=plastic)](https://discord.gg/NfH6mGFuAB)
[![Docker Cloud Build Status](https://img.shields.io/docker/cloud/build/meisnate12/plex-meta-manager?style=plastic)](https://hub.docker.com/r/meisnate12/plex-meta-manager)
[![Read the Docs](https://img.shields.io/readthedocs/plex-meta-manager?style=plastic)](https://metamanager.wiki)
[![Wiki](https://img.shields.io/readthedocs/plex-meta-manager?style=plastic)](https://metamanager.wiki)
[![Sponsor or Donate](https://img.shields.io/badge/-Sponsor_or_Donate-blueviolet?style=plastic)](https://github.com/sponsors/meisnate12)
Plex Meta Manager is an open source Python 3 project that has been designed to ease the creation and maintenance of metadata, collections, and playlists within a Plex Media Server. The script is designed to be run continuously and be able to update information based on sources outside your plex environment. Plex Meta Manager supports Movie/TV/Music libraries and Playlists.
@ -21,7 +21,7 @@ Plex Meta Manager is an open source Python 3 project that has been designed to e
3. After that you can start updating Metadata and building automatic Collections by creating a [Metadata File](https://metamanager.wiki/en/latest/metadata/metadata.html) for each Library you want to interact with.
4. After that, explore the Wiki to see all the different Collection Builders that can be used to create collections.
4. After that, explore the [Wiki](https://metamanager.wiki/) to see all the different Collection Builders that can be used to create collections.
## Walkthroughs
@ -39,7 +39,7 @@ If you find steps 1-3 above daunting, there are some walkthroughs available that
The [develop](https://github.com/meisnate12/Plex-Meta-Manager/tree/develop) branch has the most updated **documented** fixes and enhancements to Plex Meta Manager. This version is tested and documented to some degree, but it is still an active development branch, so there may be rough edges.
If switching to the develop build, it is recommended to also use the [develop](https://metamanager.wiki/en/develop/) branch of the wiki, which documents any changes made from the Master build.
If switching to the develop build, it is recommended to also use the [develop branch of the wiki](https://metamanager.wiki/en/develop/), which documents any changes made from the Master build.
### Nightly
@ -81,7 +81,7 @@ Before posting on GitHub about an enhancement, error, or configuration question
If you are unable to use the [Plex Meta Manager Discord Server](https://discord.gg/NfH6mGFuAB), please follow this guidance:
* If you have an idea for how to enhance Plex Meta Manager please open a new [Feature Request](https://github.com/meisnate12/Plex-Meta-Manager/issues/new?assignees=meisnate12&labels=status%3Anot-yet-viewed%2C+enhancement&2.feature_request.yml&title=%5BFeature%5D%3A+).
* If you're getting an Error please update to the latest version and then open a [Bug Report](https://github.com/meisnate12/Plex-Meta-Manager/issues/new?assignees=meisnate12&labels=status%3Anot-yet-viewed%2C+bug&template=1.bug_report.yml&title=%5BBug%5D%3A++) if the error persists.
* If you see a mistake/typo with the Plex Meta Manager Wiki or have an idea of how we can improve it please open a [Wiki Request](https://github.com/meisnate12/Plex-Meta-Manager/issues/new?assignees=meisnate12&labels=status%3Anot-yet-viewed%2C+documentation&template=3.docs_request.yml&title=%5BDocs%5D%3A+)
* If you see a mistake/typo with the [Plex Meta Manager Wiki](https://metamanager.wiki/) or have an idea of how we can improve it please open a [Wiki Request](https://github.com/meisnate12/Plex-Meta-Manager/issues/new?assignees=meisnate12&labels=status%3Anot-yet-viewed%2C+documentation&template=3.docs_request.yml&title=%5BDocs%5D%3A+)
* If you have a metadata configuration query please post in the [Discussions](https://github.com/meisnate12/Plex-Meta-Manager/discussions).
## Contributing

View file

@ -1 +1 @@
1.16.5-develop105
1.16.5-develop106

View file

@ -36,6 +36,7 @@ libraries:
- git: meisnate12/ShowCharts
- git: meisnate12/Networks
overlay_path:
- remove_overlays: false
- file: config/Overlays.yml
TV Shows On Second Plex:
library_name: TV Shows
@ -121,6 +122,15 @@ plex:
The `metadata_path` attribute is used to define [Metadata Files](../metadata/metadata) by specifying the path type and path of the files that will be executed against the parent library. See [Path Types](paths) for how to define them.
```yaml
libraries:
TV Shows:
metadata_path:
- file: config/TV Shows.yml
- git: meisnate12/ShowCharts
- git: meisnate12/Networks
```
By default, when `metadata_path` is missing the script will look within the root PMM directory for a metadata file called `<MAPPING_NAME>.yml`. In this example, Plex Meta Manager will look for a file named `TV Shows.yml`.
```yaml

View file

@ -6,6 +6,8 @@ Whilst it is possible to have `python plex-meta-manager.py` running in an open w
Instead, it is recommended to set an automated scheduling service so that Plex Meta Manager can run in the background when scheduled to without any visible impact to the user (other than the Plex libraries and playlists updating).
**To control how individual parts of PMM are scheduled see the [Schedule detail](../../metadata/details/schedule)**
## Docker Run
<details>

View file

@ -8,7 +8,7 @@
[![Discord](https://img.shields.io/discord/822460010649878528?label=Discord&style=plastic)](https://discord.gg/NfH6mGFuAB)
[![Docker Cloud Build Status](https://img.shields.io/docker/cloud/build/meisnate12/plex-meta-manager?style=plastic)](https://hub.docker.com/r/meisnate12/plex-meta-manager)
[![Read the Docs](https://img.shields.io/readthedocs/plex-meta-manager?style=plastic)](https://metamanager.wiki)
[![Wiki](https://img.shields.io/readthedocs/plex-meta-manager?style=plastic)](https://metamanager.wiki)
[![Sponsor or Donate](https://img.shields.io/badge/-Sponsor_or_Donate-blueviolet?style=plastic)](https://github.com/sponsors/meisnate12)
Plex Meta Manager is an open source Python 3 project that has been designed to ease the creation and maintenance of metadata, collections, and playlists within a Plex Media Server. The script is designed to be run continuously and be able to update information based on sources outside your plex environment. Plex Meta Manager supports Movie/TV/Music libraries and Playlists.
@ -21,7 +21,7 @@ Plex Meta Manager is an open source Python 3 project that has been designed to e
3. After that you can start updating Metadata and building automatic Collections by creating a [Metadata File](metadata/metadata) for each Library you want to interact with.
4. After that, explore the Wiki to see all the different Collection Builders that can be used to create collections.
4. After that, explore the [Wiki](https://metamanager.wiki/) to see all the different Collection Builders that can be used to create collections.
## Walkthroughs
@ -66,7 +66,7 @@ git checkout master
<br/>
````
If switching to the develop build, it is recommended to also use the [develop](https://metamanager.wiki/en/develop/) branch of the wiki, which documents any changes made from the Master build.
If switching to the develop build, it is recommended to also use the [develop branch of the wiki](https://metamanager.wiki/en/develop/), which documents any changes made from the Master build.
### Nightly
@ -136,7 +136,7 @@ Before posting on GitHub about an enhancement, error, or configuration question
If you are unable to use the [Plex Meta Manager Discord Server](https://discord.gg/NfH6mGFuAB), please follow this guidance:
* If you have an idea for how to enhance Plex Meta Manager please open a new [Feature Request](https://github.com/meisnate12/Plex-Meta-Manager/issues/new?assignees=meisnate12&labels=status%3Anot-yet-viewed%2C+enhancement&template=feature_request.md&title=Feature+Request%3A+).
* If you're getting an Error please update to the latest version and then open a [Bug Report](https://github.com/meisnate12/Plex-Meta-Manager/issues/new?assignees=meisnate12&labels=status%3Anot-yet-viewed%2C+bug&template=bug_report.md&title=Bug%3A+) if the error persists.
* If you see a mistake/typo with the Plex Meta Manager Wiki or have an idea of how we can improve it please open a [Wiki Request](https://github.com/meisnate12/Plex-Meta-Manager/issues/new?assignees=meisnate12&labels=status%3Anot-yet-viewed%2C+documentation&template=3.docs_request.yml&title=%5BDocs%5D%3A+)
* If you see a mistake/typo with the [Plex Meta Manager Wiki](https://metamanager.wiki/) or have an idea of how we can improve it please open a [Wiki Request](https://github.com/meisnate12/Plex-Meta-Manager/issues/new?assignees=meisnate12&labels=status%3Anot-yet-viewed%2C+documentation&template=3.docs_request.yml&title=%5BDocs%5D%3A+)
* If you have a metadata configuration query please post in the [Discussions](https://github.com/meisnate12/Plex-Meta-Manager/discussions).
## Contributing

View file

@ -122,7 +122,7 @@ dynamic_collections:
## Metadata Attributes
Plex Meta Manager can automatically update items in Plex based on what's defined within the `metadata` attribute.
Plex Meta Manager can automatically update items in Plex [Movie](metadata/movie), [Show](metadata/movie), and [Music](metadata/movie) Libraries based on what's defined within the `metadata` attribute.
Each metadata requires its own section within the `metadata` attribute. Each item is defined by the mapping name which must be the same as the item name in the library unless an `alt_title` is specified.

BIN
fonts/Comfortaa-Bold.ttf Normal file

Binary file not shown.

BIN
fonts/Comfortaa-Light.ttf Normal file

Binary file not shown.

BIN
fonts/Comfortaa-Medium.ttf Normal file

Binary file not shown.

BIN
fonts/Comfortaa-Regular.ttf Normal file

Binary file not shown.

Binary file not shown.

View file

@ -127,6 +127,8 @@ class ConfigFile:
if "settings" in self.data["libraries"][library] and self.data["libraries"][library]["settings"]:
if "collection_minimum" in self.data["libraries"][library]["settings"]:
self.data["libraries"][library]["settings"]["minimum_items"] = self.data["libraries"][library]["settings"].pop("collection_minimum")
if "save_missing" in self.data["libraries"][library]["settings"]:
self.data["libraries"][library]["settings"]["save_report"] = self.data["libraries"][library]["settings"].pop("save_missing")
if "radarr" in self.data["libraries"][library] and self.data["libraries"][library]["radarr"]:
if "add" in self.data["libraries"][library]["radarr"]:
self.data["libraries"][library]["radarr"]["add_missing"] = self.data["libraries"][library]["radarr"].pop("add")
@ -156,6 +158,8 @@ class ConfigFile:
temp["minimum_items"] = temp.pop("collection_minimum")
if "playlist_sync_to_user" in temp:
temp["playlist_sync_to_users"] = temp.pop("playlist_sync_to_user")
if "save_missing" in temp:
temp["save_report"] = temp.pop("save_missing")
self.data["settings"] = temp
if "webhooks" in self.data:
temp = self.data.pop("webhooks")

View file

@ -11,27 +11,43 @@ class Letterboxd:
def __init__(self, config):
self.config = config
def _parse_list(self, list_url, language):
if self.config.trace_mode:
logger.debug(f"URL: {list_url}")
def _parse_page(self, list_url, language):
list_url = list_url.replace("https://letterboxd.com/films", "https://letterboxd.com/films/ajax")
response = self.config.get_html(list_url, headers=util.header(language))
letterboxd_ids = response.xpath("//li[contains(@class, 'poster-container') or contains(@class, 'film-detail')]/div/@data-film-id")
letterboxd_ids = response.xpath(
"//li[contains(@class, 'poster-container') or contains(@class, 'film-detail')]/div/@data-film-id")
items = []
for letterboxd_id in letterboxd_ids:
slugs = response.xpath(f"//div[@data-film-id='{letterboxd_id}']/@data-film-slug")
notes = response.xpath(f"//div[@data-film-id='{letterboxd_id}']/parent::li/div[@class='film-detail-content']/div/p/text()") if list_url.endswith(("/detail", "/detail/")) else None
ratings = response.xpath(f"//div[@data-film-id='{letterboxd_id}']/parent::li/div[@class='film-detail-content']//span[contains(@class, 'rating')]/@class") if list_url.endswith(("/detail", "/detail/")) else None
years = response.xpath(f"//div[@data-film-id='{letterboxd_id}']/parent::li/div[@class='film-detail-content']/h2/small/a/text()") if list_url.endswith(("/detail", "/detail/")) else None
comments = response.xpath(
f"//div[@data-film-id='{letterboxd_id}']/parent::li/div[@class='film-detail-content']/div/p/text()")
ratings = response.xpath(
f"//div[@data-film-id='{letterboxd_id}']/parent::li/div[@class='film-detail-content']//span[contains(@class, 'rating')]/@class")
years = response.xpath(
f"//div[@data-film-id='{letterboxd_id}']/parent::li/div[@class='film-detail-content']/h2/small/a/text()")
rating = None
if ratings:
match = re.search("rated-(\\d+)", ratings[0])
if match:
rating = int(match.group(1))
items.append((letterboxd_id, slugs[0], int(years[0]) if years else None, notes[0] if notes else None, rating))
items.append((letterboxd_id, slugs[0],
int(years[0]) if years else None,
comments[0] if comments else None,
rating
))
next_url = response.xpath("//a[@class='next']/@href")
if len(next_url) > 0:
return items, next_url
def _parse_list(self, list_url, limit, language):
if self.config.trace_mode:
logger.debug(f"URL: {list_url}")
items, next_url = self._parse_page(list_url, language)
while len(next_url) > 0:
time.sleep(2)
items.extend(self._parse_list(f"{base_url}{next_url[0]}", language))
new_items, next_url = self._parse_page(f"{base_url}{next_url[0]}", language)
items.extend(new_items)
if limit and len(items) >= limit:
return items[:limit]
return items
def _tmdb(self, letterboxd_url, language):
@ -60,13 +76,14 @@ class Letterboxd:
dict_methods = {dm.lower(): dm for dm in letterboxd_dict}
final = {
"url": util.parse(err_type, "url", letterboxd_dict, methods=dict_methods, parent="letterboxd_list").strip(),
"limit": util.parse(err_type, "limit", letterboxd_dict, methods=dict_methods, datatype="int", parent="letterboxd_list", default=0) if "limit" in dict_methods else 0,
"note": util.parse(err_type, "note", letterboxd_dict, methods=dict_methods, parent="letterboxd_list") if "note" in dict_methods else None,
"rating": util.parse(err_type, "rating", letterboxd_dict, methods=dict_methods, datatype="int", parent="letterboxd_list", maximum=100, range_split="-") if "rating" in dict_methods else None,
"year": util.parse(err_type, "year", letterboxd_dict, methods=dict_methods, datatype="int", parent="letterboxd_list", minimum=1000, maximum=3000, range_split="-") if "year" in dict_methods else None
}
if not final["url"].startswith(base_url):
raise Failed(f"{err_type} Error: {final['url']} must begin with: {base_url}")
elif not self._parse_list(final["url"], language):
elif not self._parse_page(final["url"], language)[0]:
raise Failed(f"{err_type} Error: {final['url']} failed to parse")
valid_lists.append(final)
return valid_lists
@ -74,7 +91,7 @@ class Letterboxd:
def get_tmdb_ids(self, method, data, language):
if method == "letterboxd_list":
logger.info(f"Processing Letterboxd List: {data}")
items = self._parse_list(data["url"], language)
items = self._parse_list(data["url"], data["limit"], language)
total_items = len(items)
if total_items > 0:
ids = []

View file

@ -515,6 +515,13 @@ class MetadataFile(DataFile):
methods["key_name_override"] = methods.pop("pre_format_override")
title_override = util.parse("Config", "title_override", dynamic, parent=map_name, methods=methods, datatype="strdict") if "title_override" in methods else {}
key_name_override = util.parse("Config", "key_name_override", dynamic, parent=map_name, methods=methods, datatype="strdict") if "key_name_override" in methods else {}
test_override = []
for k, v in key_name_override.items():
if v in test_override:
logger.warning(f"Config Error: {v} can only be used once skipping {k}: {v}")
key_name_override.pop(k)
else:
test_override.append(v)
test = util.parse("Config", "test", dynamic, parent=map_name, methods=methods, default=False, datatype="bool") if "test" in methods else False
sync = util.parse("Config", "sync", dynamic, parent=map_name, methods=methods, default=False, datatype="bool") if "sync" in methods else False
if "<<library_type>>" in title_format:

View file

@ -169,8 +169,6 @@ class Overlays:
new_poster = Image.open(poster.location if poster else has_original) \
.convert("RGB").resize((image_width, image_height), Image.ANTIALIAS)
overlay_image = Image.new('RGBA', new_poster.size, (255, 255, 255, 0))
drawing = ImageDraw.Draw(overlay_image)
if blur_num > 0:
new_poster = new_poster.filter(ImageFilter.GaussianBlur(blur_num))
for over_name in normal_overlays:
@ -178,43 +176,24 @@ class Overlays:
if not overlay.has_coordinates():
new_poster = new_poster.resize(overlay.image.size, Image.ANTIALIAS)
new_poster.paste(overlay.image, overlay.get_coordinates(image_width, image_height), overlay.image)
if text_names:
for over_name in text_names:
overlay = properties[over_name]
text = over_name[5:-1]
if text in [f"{a}{s}" for a in ["audience_rating", "critic_rating", "user_rating"] for s in ["", "%"]]:
per = text.endswith("%")
rating_type = text[:-1] if per else text
actual = plex.attribute_translation[rating_type]
if not hasattr(item, actual) or getattr(item, actual) is None:
logger.error(f"Overlay Error: No {rating_type} found")
continue
text = getattr(item, actual)
if self.config.Cache:
self.config.Cache.update_overlay_ratings(item.ratingKey, rating_type, text)
if per:
text = f"{int(text * 10)}%"
x_cord, y_cord = overlay.get_coordinates(image_width, image_height, text=str(text))
_, _, width, height = overlay.get_text_size(str(text))
if overlay.back_color:
cords = (
x_cord - overlay.back_padding,
y_cord - overlay.back_padding,
x_cord + (overlay.back_width if overlay.back_width else width) + overlay.back_padding,
y_cord + (overlay.back_height if overlay.back_height else height) + overlay.back_padding
)
if overlay.back_width:
x_cord = int(x_cord + (overlay.back_width - width) / 2)
y_cord = int(y_cord + (overlay.back_height - height) / 2)
if overlay.back_radius:
drawing.rounded_rectangle(cords, fill=overlay.back_color, outline=overlay.back_line_color,
width=overlay.back_line_width, radius=overlay.back_radius)
else:
drawing.rectangle(cords, fill=overlay.back_color, outline=overlay.back_line_color,
width=overlay.back_line_width)
drawing.text((x_cord, y_cord), str(text), font=overlay.font, fill=overlay.font_color, anchor='lt')
for over_name in text_names:
overlay = properties[over_name]
text = over_name[5:-1]
if text in [f"{a}{s}" for a in ["audience_rating", "critic_rating", "user_rating"] for s in ["", "%"]]:
per = text.endswith("%")
rating_type = text[:-1] if per else text
actual = plex.attribute_translation[rating_type]
if not hasattr(item, actual) or getattr(item, actual) is None:
logger.warning(f"Overlay Warning: No {rating_type} found")
continue
text = getattr(item, actual)
if self.config.Cache:
self.config.Cache.update_overlay_ratings(item.ratingKey, rating_type, text)
if per:
text = f"{int(text * 10)}%"
overlay_image = overlay.get_text_overlay(text, image_width, image_height)
else:
overlay_image = overlay.landscape if isinstance(item, Episode) else overlay.image
new_poster.paste(overlay_image, (0, 0), overlay_image)
temp = os.path.join(self.library.overlay_folder, f"temp.png")
new_poster.save(temp, "PNG")

View file

@ -151,6 +151,7 @@ method_alias = {
"labels": "label",
"collection_minimum": "minimum_items",
"playlist_minimum": "minimum_items",
"save_missing": "save_report",
"rating": "critic_rating",
"show_user_rating": "user_rating",
"video_resolution": "resolution",

View file

@ -835,6 +835,7 @@ class Overlay:
self.keys = []
self.updated = False
self.image = None
self.landscape = None
self.group = None
self.weight = None
self.path = None
@ -993,6 +994,16 @@ class Overlay:
self.back_height = parse("Overlay", "back_height", self.data["back_height"], datatype="int", parent="overlay")
if (self.back_width and not self.back_height) or (self.back_height and not self.back_width):
raise Failed(f"Overlay Error: overlay attributes back_width and back_height must be used together")
text = self.name[5:-1]
if text not in [f"{a}{s}" for a in ["audience_rating", "critic_rating", "user_rating"] for s in ["", "%"]]:
self.image = self.get_text_overlay(text, 1000, 1500)
self.landscape = self.get_text_overlay(text, 1920, 1080)
else:
if "|" in self.name:
raise Failed(f"Overlay Error: Overlay Name: {self.name} cannot contain '|'")
@ -1013,6 +1024,29 @@ class Overlay:
except OSError:
raise Failed(f"Overlay Error: overlay image {self.path} failed to load")
def get_text_overlay(self, text, image_width, image_height):
overlay_image = Image.new("RGBA", (image_width, image_height), (255, 255, 255, 0))
drawing = ImageDraw.Draw(overlay_image)
x_cord, y_cord = self.get_coordinates(image_width, image_height, text=str(text))
_, _, width, height = self.get_text_size(str(text))
if self.back_color:
cords = (
x_cord - self.back_padding,
y_cord - self.back_padding,
x_cord + (self.back_width if self.back_width else width) + self.back_padding,
y_cord + (self.back_height if self.back_height else height) + self.back_padding
)
if self.back_width:
x_cord = x_cord + (self.back_width - width) // 2
y_cord = y_cord + (self.back_height - height) // 2
if self.back_radius:
drawing.rounded_rectangle(cords, fill=self.back_color, outline=self.back_line_color, width=self.back_line_width, radius=self.back_radius)
else:
drawing.rectangle(cords, fill=self.back_color, outline=self.back_line_color, width=self.back_line_width)
drawing.text((x_cord, y_cord), str(text), font=self.font, fill=self.font_color, anchor="lt")
return overlay_image
def get_overlay_compare(self):
output = self.name
if self.group: