[106] small updates

pull/880/head
meisnate12 2 years ago
parent 9d94f00ac7
commit 5369dc4fd8

@ -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.

@ -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

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

@ -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

@ -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>

@ -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

@ -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.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

@ -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")

@ -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 = []

@ -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:

@ -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")

@ -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",

@ -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:

Loading…
Cancel
Save