fix: collection hub discovery for deleted collections

stale collections in plex hubs list (collections that have been deleted in plex, but remain in plex
hubs list due to caching) are now correctly deleted when the clean up button is pressed
This commit is contained in:
Tom Wheeler
2025-09-03 19:44:27 +12:00
parent eda801f0b7
commit edb93fecdc
13 changed files with 375 additions and 77 deletions

View File

@@ -27,8 +27,6 @@ RUN yarn build
# remove development dependencies
RUN yarn install --production --ignore-scripts --prefer-offline
# Copy service logos to public directory for poster generation
RUN mkdir -p public/services && cp -r src/assets/services/* public/services/
RUN rm -rf src server .next/cache
RUN mkdir -p config && touch config/DOCKER

1
public/services/imdb.svg Normal file
View File

@@ -0,0 +1 @@
<svg version="1.1" viewBox="0 0 575 289.83" xmlns="http://www.w3.org/2000/svg"><path fill="#f6c700" d="m575 24.91c-1.56-12.76-11.03-22.93-23.09-24.91h-528.59c-13.21 2.17-23.32 14.16-23.32 28.61v232.25c0 16 12.37 28.97 27.64 28.97h519.95c14.06 0 25.67-11.01 27.41-25.26v-239.66z"/><path stroke="#000" d="m69.35 58.24h45.63v175.65h-45.63v-175.65z"/><path stroke="#000" d="m201.2 139.15c-3.92-26.77-6.1-41.65-6.53-44.62-1.91-14.33-3.73-26.8-5.47-37.44h-59.16v175.65h39.97l0.14-115.98 16.82 115.98h28.47l15.95-118.56 0.15 118.56h39.84v-175.65h-59.61l-10.57 82.06z"/><path stroke="#000" d="m346.71 93.63c0.5 2.24 0.76 7.32 0.76 15.26v68.1c0 11.69-0.76 18.85-2.27 21.49-1.52 2.64-5.56 3.95-12.11 3.95v-115.3c4.97 0 8.36 0.53 10.16 1.57 1.8 1.05 2.96 2.69 3.46 4.93zm20.61 137.32c5.43-1.19 9.99-3.29 13.69-6.28 3.69-3 6.28-7.15 7.76-12.46 1.49-5.3 2.37-15.83 2.37-31.58v-61.68c0-16.62-0.65-27.76-1.66-33.42-1.02-5.67-3.55-10.82-7.6-15.44-4.06-4.62-9.98-7.94-17.76-9.96-7.79-2.02-20.49-3.04-42.58-3.04h-34.04v175.65h55.28c12.74-0.4 20.92-0.99 24.54-1.79z"/><path stroke="#000" d="m464.76 204.7c-0.84 2.23-4.52 3.36-7.3 3.36-2.72 0-4.53-1.08-5.45-3.25-0.92-2.16-1.37-7.09-1.37-14.81v-46.42c0-8 0.4-12.99 1.21-14.98 0.8-1.97 2.56-2.97 5.28-2.97 2.78 0 6.51 1.13 7.47 3.4 0.95 2.27 1.43 7.12 1.43 14.55v45.01c-0.29 9.25-0.71 14.62-1.27 16.11zm-58.08 26.51h41.08c1.71-6.71 2.65-10.44 2.84-11.19 3.72 4.5 7.81 7.88 12.3 10.12 4.47 2.25 11.16 3.37 16.34 3.37 7.21 0 13.43-1.89 18.68-5.68 5.24-3.78 8.58-8.26 10-13.41 1.42-5.16 2.13-13 2.13-23.54v-49.28c0-10.6-0.24-17.52-0.71-20.77s-1.87-6.56-4.2-9.95-5.72-6.02-10.16-7.9-9.68-2.82-15.72-2.82c-5.25 0-11.97 1.05-16.45 3.12-4.47 2.07-8.53 5.21-12.17 9.42v-57.14h-43.96v175.65z"/></svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="500px" height="500px" viewBox="0 0 500 500" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 52.2 (67145) - http://www.bohemiancoding.com/sketch -->
<title>letterboxd-decal-dots-pos-rgb</title>
<desc>Created with Sketch.</desc>
<defs>
<rect id="path-1" x="0" y="0" width="129.847328" height="141.389313"></rect>
<rect id="path-3" x="0" y="0" width="129.847328" height="141.389313"></rect>
</defs>
<g id="letterboxd-decal-dots-pos-rgb" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<circle id="Circle" fill="#202830" cx="250" cy="250" r="250"></circle>
<g id="dots-neg" transform="translate(61.000000, 180.000000)">
<g id="Dots">
<ellipse id="Green" fill="#00E054" cx="189" cy="69.9732824" rx="70.0786517" ry="69.9732824"></ellipse>
<g id="Blue" transform="translate(248.152672, 0.000000)">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<g id="Mask"></g>
<ellipse fill="#40BCF4" mask="url(#mask-2)" cx="59.7686766" cy="69.9732824" rx="70.0786517" ry="69.9732824"></ellipse>
</g>
<g id="Orange">
<mask id="mask-4" fill="white">
<use xlink:href="#path-3"></use>
</mask>
<g id="Mask"></g>
<ellipse fill="#FF8000" mask="url(#mask-4)" cx="70.0786517" cy="69.9732824" rx="70.0786517" ry="69.9732824"></ellipse>
</g>
<path d="M129.539326,107.022244 C122.810493,96.2781677 118.921348,83.5792213 118.921348,69.9732824 C118.921348,56.3673435 122.810493,43.6683972 129.539326,32.9243209 C136.268159,43.6683972 140.157303,56.3673435 140.157303,69.9732824 C140.157303,83.5792213 136.268159,96.2781677 129.539326,107.022244 Z" id="Overlap" fill="#FFFFFF"></path>
<path d="M248.460674,32.9243209 C255.189507,43.6683972 259.078652,56.3673435 259.078652,69.9732824 C259.078652,83.5792213 255.189507,96.2781677 248.460674,107.022244 C241.731841,96.2781677 237.842697,83.5792213 237.842697,69.9732824 C237.842697,56.3673435 241.731841,43.6683972 248.460674,32.9243209 Z" id="Overlap" fill="#FFFFFF"></path>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="96" height="96" fill="none" viewBox="0 0 96 96"><path fill="url(#paint0_linear)" fill-rule="evenodd" d="M48 96C74.5097 96 96 74.5097 96 48C96 21.4903 74.5097 0 48 0C21.4903 0 0 21.4903 0 48C0 74.5097 21.4903 96 48 96ZM80.0001 52C80.0001 67.464 67.4641 80 52.0001 80C36.5361 80 24.0001 67.464 24.0001 52C24.0001 49.1303 24.4318 46.3615 25.2338 43.7548C27.4288 48.6165 32.3194 52 38.0001 52C45.7321 52 52.0001 45.732 52.0001 38C52.0001 32.3192 48.6166 27.4287 43.755 25.2337C46.3616 24.4317 49.1304 24 52.0001 24C67.4641 24 80.0001 36.536 80.0001 52Z" clip-rule="evenodd"/><path fill="#131928" fill-rule="evenodd" d="M80.0002 52C80.0002 67.464 67.4642 80 52.0002 80C36.864 80 24.5329 67.9897 24.017 52.9791C24.0057 53.318 24 53.6583 24 54C24 70.5685 37.4315 84 54 84C70.5685 84 84 70.5685 84 54C84 37.4315 70.5685 24 54 24C53.6597 24 53.3207 24.0057 52.9831 24.0169C67.9919 24.5347 80.0002 36.865 80.0002 52Z" clip-rule="evenodd" opacity=".2"/><path fill="url(#paint1_linear)" fill-rule="evenodd" d="M48 12C28.1177 12 12 28.1177 12 48C12 50.2091 10.2091 52 8 52C5.79086 52 4 50.2091 4 48C4 23.6995 23.6995 4 48 4C50.2091 4 52 5.79086 52 8C52 10.2091 50.2091 12 48 12Z" clip-rule="evenodd"/><defs><linearGradient id="paint0_linear" x1="48" x2="117.5" y1="0" y2="69.5" gradientUnits="userSpaceOnUse"><stop stop-color="#C395FC"/><stop offset="1" stop-color="#4F65F5"/></linearGradient><linearGradient id="paint1_linear" x1="28" x2="28" y1="8" y2="48" gradientUnits="userSpaceOnUse"><stop stop-color="#fff" stop-opacity=".4"/><stop offset="1" stop-color="#fff" stop-opacity="0"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

31
public/services/plex.svg Normal file
View File

@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 361 157">
<defs>
<style>
.cls-1 {
fill: #fff;
}
.cls-2 {
fill: #eaaf20;
}
</style>
</defs>
<!-- Generator: Adobe Illustrator 28.7.1, SVG Export Plug-In . SVG Version: 1.2.0 Build 142) -->
<g>
<g id="Layer_1">
<path id="path4" class="cls-1" d="M60.6,28.8c-14.3,0-23.5,3.9-31.3,13v-10H1.6v123.7s.5.2,1.9.5c1.9.5,12.1,2.5,19.6-3.4,6.5-5.3,8-11.4,8-18.3v-17.8c8,8,17,11.4,29.6,11.4,27.2,0,48-20.8,48-48.4s-20.1-50.7-48.3-50.7h0ZM55.2,104.3c-15.3,0-27.4-11.9-27.4-26.3s14.3-25.6,27.4-25.6,27.4,11.2,27.4,25.8-12.1,26-27.4,26Z"/>
<path id="path6" class="cls-1" d="M148.1,76.5c0,10.7,1.2,23.7,12.4,37.9.2.2.7.9.7.9-4.6,7.3-10.2,12.3-17.7,12.3s-11.6-3-16.5-8c-5.1-5.5-7.5-12.6-7.5-20.1V.9h28.4l.2,75.6Z"/>
<polygon id="polygon8" class="cls-2" points="287.6 78.3 254.1 31.7 288.6 31.7 321.8 78.3 288.6 124.6 254.1 124.6 287.6 78.3"/>
<polygon id="polygon10" class="cls-1" points="330.8 73 360.6 31.7 326.2 31.7 313.8 48.9 330.8 73"/>
<path id="path12" class="cls-1" d="M313.8,107.7l5.8,7.5c5.6,8.2,12.9,12.3,21.3,12.3,9-.2,15.3-7.5,17.7-10.3,0,0-4.4-3.7-9.9-9.8-7.5-8.2-17.5-23.3-17.7-24l-17.2,24.2Z"/>
<path id="path16" class="cls-1" d="M228.7,97.9c-5.8,5-9.7,7.8-17.7,7.8-14.3,0-22.6-9.6-23.8-20.1h75.9c.5-1.4.7-3.2.7-6.2,0-29-22.6-50.7-52.2-50.7s-51.2,22.1-51.2,49.8,23,49.1,51.9,49.1,37.6-10.7,47.1-29.7h-30.8ZM211.9,50.7c12.6,0,22.1,7.8,24.3,18h-48c2.4-10.7,11.4-18,23.8-18h0Z"/>
<path id="path4-2" data-name="path4" class="cls-1" d="M59.3,28.2c-14.3,0-23.5,3.9-31.3,13v-10H.4v123.7s.5.2,1.9.5c1.9.5,12.1,2.5,19.6-3.4,6.5-5.3,8-11.4,8-18.3v-17.8c8,8,17,11.4,29.6,11.4,27.2,0,48-20.8,48-48.4s-20.1-50.7-48.3-50.7h0ZM54,103.8c-15.3,0-27.4-11.9-27.4-26.3s14.3-25.6,27.4-25.6,27.4,11.2,27.4,25.8-12.1,26-27.4,26Z"/>
<path id="path6-2" data-name="path6" class="cls-1" d="M146.9,75.9c0,10.7,1.2,23.7,12.4,37.9.2.2.7.9.7.9-4.6,7.3-10.2,12.3-17.7,12.3s-11.6-3-16.5-8c-5.1-5.5-7.5-12.6-7.5-20.1V.4h28.4l.2,75.6Z"/>
<polygon id="polygon8-2" data-name="polygon8" class="cls-2" points="286.4 77.8 252.9 31.2 287.3 31.2 320.6 77.8 287.3 124.1 252.9 124.1 286.4 77.8"/>
<polygon id="polygon10-2" data-name="polygon10" class="cls-1" points="329.5 72.5 359.4 31.2 324.9 31.2 312.6 48.3 329.5 72.5"/>
<path id="path12-2" data-name="path12" class="cls-1" d="M312.6,107.2l5.8,7.5c5.6,8.2,12.9,12.3,21.3,12.3,9-.2,15.3-7.5,17.7-10.3,0,0-4.4-3.7-9.9-9.8-7.5-8.2-17.5-23.3-17.7-24l-17.2,24.2Z"/>
<path id="path16-2" data-name="path16" class="cls-1" d="M227.4,97.4c-5.8,5-9.7,7.8-17.7,7.8-14.3,0-22.6-9.6-23.8-20.1h75.9c.5-1.4.7-3.2.7-6.2,0-29-22.6-50.7-52.2-50.7s-51.2,22.1-51.2,49.8,23,49.1,51.9,49.1,37.6-10.7,47.1-29.7h-30.8ZM210.7,50.1c12.6,0,22.1,7.8,24.3,18h-48c2.4-10.7,11.4-18,23.8-18h0Z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@@ -0,0 +1 @@
<svg version="1.1" viewBox="0 0 1000 1115.2" xmlns="http://www.w3.org/2000/svg"><path transform="matrix(1.1348 0 0 1.1348 -.0011348 -.013738)" d="m105.76 154.15-1.263 714.59c-60.261 6.782-105.02-23.858-104.28-84.025l-0.216-594.25c2.312-188.02 175.85-231.02 280.22-154.52l530.2 314.93c74.563 53.572 88.403 151.53 49.965 218.76-6.873-52.739-29.067-83.101-73.823-113.74l-597.52-345.84c-44.756-30.639-82.453-23.58-83.286 44.109zm-54.377 751.54c44.941 15.597 90.16 8.63 128.04-13.47l621.16-353.43c36.958 53.109 28.79 105.66-16.706 135.19l-522.65 294.46c-75.673 36.68-172.98-2.127-209.85-62.757z" fill="#fff"/><path transform="matrix(1.1348 0 0 1.1348 -.0011348 -.013738)" d="m240.52 702.59 365.02-216.68-364.35-197.07z" fill="#ffc230"/></svg>

After

Width:  |  Height:  |  Size: 737 B

View File

@@ -0,0 +1 @@
<svg viewBox="0 0 216.7 216.9" xmlns="http://www.w3.org/2000/svg"><path d="M216.7 108.45c0 29.833-10.533 55.4-31.6 76.7-.7.833-1.483 1.6-2.35 2.3-3.466 3.4-7.133 6.484-11 9.25-18.267 13.467-39.367 20.2-63.3 20.2-23.967 0-45.033-6.733-63.2-20.2-4.8-3.4-9.3-7.25-13.5-11.55-16.367-16.266-26.417-35.167-30.15-56.7-.733-4.2-1.217-8.467-1.45-12.8-.1-2.4-.15-4.8-.15-7.2 0-2.533.05-4.95.15-7.25 0-.233.066-.467.2-.7 1.567-26.6 12.033-49.583 31.4-68.95C53.05 10.517 78.617 0 108.45 0c29.933 0 55.484 10.517 76.65 31.55 21.067 21.433 31.6 47.067 31.6 76.9z" clip-rule="evenodd" fill="#EEE" fill-rule="evenodd"/><path d="M194.65 42.5l-22.4 22.4C159.152 77.998 158 89.4 158 109.5c0 17.934 2.852 34.352 16.2 47.7 9.746 9.746 19 18.95 19 18.95-2.5 3.067-5.2 6.067-8.1 9-.7.833-1.483 1.6-2.35 2.3-2.533 2.5-5.167 4.817-7.9 6.95l-17.55-17.55c-15.598-15.6-27.996-17.1-48.6-17.1-19.77 0-33.223 1.822-47.7 16.3-8.647 8.647-18.55 18.6-18.55 18.6-3.767-2.867-7.333-6.034-10.7-9.5-2.8-2.8-5.417-5.667-7.85-8.6 0 0 9.798-9.848 19.15-19.2 13.852-13.853 16.1-29.916 16.1-47.85 0-17.5-2.874-33.823-15.6-46.55-8.835-8.836-21.05-21-21.05-21 2.833-3.6 5.917-7.067 9.25-10.4 2.934-2.867 5.934-5.55 9-8.05L61.1 43.85C74.102 56.852 90.767 60.2 108.7 60.2c18.467 0 35.077-3.577 48.6-17.1 8.32-8.32 19.3-19.25 19.3-19.25 2.9 2.367 5.733 4.933 8.5 7.7 3.467 3.533 6.65 7.183 9.55 10.95z" clip-rule="evenodd" fill="#3A3F51" fill-rule="evenodd"/><g clip-rule="evenodd"><path d="M78.7 114c-.2-1.167-.332-2.35-.4-3.55-.032-.667-.05-1.333-.05-2 0-.7.018-1.367.05-2 0-.067.018-.133.05-.2.435-7.367 3.334-13.733 8.7-19.1 5.9-5.833 12.984-8.75 21.25-8.75 8.3 0 15.384 2.917 21.25 8.75 5.834 5.934 8.75 13.033 8.75 21.3 0 8.267-2.916 15.35-8.75 21.25-.2.233-.416.45-.65.65-.966.933-1.982 1.783-3.05 2.55-5.065 3.733-10.916 5.6-17.55 5.6s-12.466-1.866-17.5-5.6c-1.332-.934-2.582-2-3.75-3.2-4.532-4.5-7.316-9.734-8.35-15.7z" fill="#0CF" fill-rule="evenodd"/><path d="M157.8 59.75l-15 14.65M30.785 32.526L71.65 73.25m84.6 84.25l27.808 28.78m1.855-153.894L157.8 59.75m-125.45 126l27.35-27.4" fill="none" stroke="#0CF" stroke-miterlimit="1" stroke-width="2"/><path d="M157.8 59.75l-16.95 17.2M58.97 60.604l17.2 17.15M59.623 158.43l16.75-17.4m61.928-1.396l18.028 17.945" fill="none" stroke="#0CF" stroke-miterlimit="1" stroke-width="7"/></g></svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="298.961px" height="300px" viewBox="0 0 298.961 300" enable-background="new 0 0 298.961 300" xml:space="preserve">
<g>
<image overflow="visible" opacity="0.2" width="289" height="224" xlink:href="93124B54.png" transform="matrix(1 0 0 1 4.9805 70.5)">
</image>
<g>
<path fill="#FFFFFF" d="M241.811,215.25c-10.935,0-20.375,6.382-24.809,15.623l-39.383-5.715
c-0.139-15.068-12.393-27.242-27.493-27.242c-5.385,0-10.405,1.554-14.646,4.229l-55.963-63.414
c3.224-4.505,5.127-10.02,5.127-15.981c0-15.188-12.312-27.5-27.5-27.5s-27.5,12.312-27.5,27.5s12.312,27.5,27.5,27.5
c4.677,0,9.079-1.171,12.934-3.23l56.56,64.089c-2.544,4.168-4.012,9.064-4.012,14.307c0,15.188,12.312,27.5,27.5,27.5
c10.872,0,20.269-6.311,24.731-15.467l39.463,5.727c0.229,14.99,12.443,27.074,27.49,27.074c15.188,0,27.5-12.313,27.5-27.5
S256.998,215.25,241.811,215.25z"/>
</g>
</g>
<g>
<image overflow="visible" opacity="0.2" width="289" height="289" xlink:href="93124B55.png" transform="matrix(1 0 0 1 4.9805 5.5)">
</image>
<g>
<path fill="#E5A00D" d="M241.811,29.75c-15.188,0-27.5,12.312-27.5,27.5c0,7.48,2.99,14.258,7.836,19.216l-60.943,86.113
c-3.389-1.493-7.135-2.329-11.076-2.329c-15.188,0-27.5,12.313-27.5,27.5c0,2.704,0.397,5.314,1.125,7.783l-46.306,28.668
c-5.028-5.5-12.261-8.951-20.3-8.951c-15.188,0-27.5,12.313-27.5,27.5s12.312,27.5,27.5,27.5s27.5-12.313,27.5-27.5
c0-2.627-0.376-5.165-1.064-7.57l46.396-28.724c5.022,5.407,12.189,8.794,20.15,8.794c15.188,0,27.5-12.313,27.5-27.5
c0-6.701-2.399-12.84-6.382-17.611l61.515-86.92c2.837,0.988,5.88,1.532,9.052,1.532c15.188,0,27.5-12.312,27.5-27.5
S256.998,29.75,241.811,29.75z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

1
public/services/tmdb.svg Normal file
View File

@@ -0,0 +1 @@
<svg viewBox="0 0 185.04 133.4" xmlns="http://www.w3.org/2000/svg"><defs><style>.cls-1{fill:url(#a)}</style><linearGradient id="a" x2="185.04" y1="66.7" y2="66.7" gradientUnits="userSpaceOnUse"><stop stop-color="#90cea1" offset="0"/><stop stop-color="#3cbec9" offset=".56"/><stop stop-color="#00b3e5" offset="1"/></linearGradient></defs><path class="cls-1" d="M51.06,66.7h0A17.67,17.67,0,0,1,68.73,49h-.1A17.67,17.67,0,0,1,86.3,66.7h0A17.67,17.67,0,0,1,68.63,84.37h.1A17.67,17.67,0,0,1,51.06,66.7Zm82.67-31.33h32.9A17.67,17.67,0,0,0,184.3,17.7h0A17.67,17.67,0,0,0,166.63,0h-32.9A17.67,17.67,0,0,0,116.06,17.7h0A17.67,17.67,0,0,0,133.73,35.37Zm-113,98h63.9A17.67,17.67,0,0,0,102.3,115.7h0A17.67,17.67,0,0,0,84.63,98H20.73A17.67,17.67,0,0,0,3.06,115.7h0A17.67,17.67,0,0,0,20.73,133.37Zm83.92-49h6.25L125.5,49h-8.35l-8.9,23.2h-.1L99.4,49H90.5Zm32.45,0h7.8V49h-7.8Zm22.2,0h24.95V77.2H167.1V70h15.35V62.8H167.1V56.2h16.25V49h-24ZM10.1,35.4h7.8V6.9H28V0H0V6.9H10.1ZM39,35.4h7.8V20.1H61.9V35.4h7.8V0H61.9V13.2H46.75V0H39Zm41.25,0h25V28.2H88V21h15.35V13.8H88V7.2h16.25V0h-24Zm-79,49H9V57.25h.1l9,27.15H24l9.3-27.15h.1V84.4h7.8V49H29.45l-8.2,23.1h-.1L13,49H1.2Zm112.09,49H126a24.59,24.59,0,0,0,7.56-1.15,19.52,19.52,0,0,0,6.35-3.37,16.37,16.37,0,0,0,4.37-5.5A16.91,16.91,0,0,0,146,115.8a18.5,18.5,0,0,0-1.68-8.25,15.1,15.1,0,0,0-4.52-5.53A18.55,18.55,0,0,0,133.07,99,33.54,33.54,0,0,0,125,98H113.29Zm7.81-28.2h4.6a17.43,17.43,0,0,1,4.67.62,11.68,11.68,0,0,1,3.88,1.88,9,9,0,0,1,2.62,3.18,9.87,9.87,0,0,1,1,4.52,11.92,11.92,0,0,1-1,5.08,8.69,8.69,0,0,1-2.67,3.34,10.87,10.87,0,0,1-4,1.83,21.57,21.57,0,0,1-5,.55H121.1Zm36.14,28.2h14.5a23.11,23.11,0,0,0,4.73-.5,13.38,13.38,0,0,0,4.27-1.65,9.42,9.42,0,0,0,3.1-3,8.52,8.52,0,0,0,1.2-4.68,9.16,9.16,0,0,0-.55-3.2,7.79,7.79,0,0,0-1.57-2.62,8.38,8.38,0,0,0-2.45-1.85,10,10,0,0,0-3.18-1v-.1a9.28,9.28,0,0,0,4.43-2.82,7.42,7.42,0,0,0,1.67-5,8.34,8.34,0,0,0-1.15-4.65,7.88,7.88,0,0,0-3-2.73,12.9,12.9,0,0,0-4.17-1.3,34.42,34.42,0,0,0-4.63-.32h-13.2Zm7.8-28.8h5.3a10.79,10.79,0,0,1,1.85.17,5.77,5.77,0,0,1,1.7.58,3.33,3.33,0,0,1,1.23,1.13,3.22,3.22,0,0,1,.47,1.82,3.63,3.63,0,0,1-.42,1.8,3.34,3.34,0,0,1-1.13,1.2,4.78,4.78,0,0,1-1.57.65,8.16,8.16,0,0,1-1.78.2H165Zm0,14.15h5.9a15.12,15.12,0,0,1,2.05.15,7.83,7.83,0,0,1,2,.55,4,4,0,0,1,1.58,1.17,3.13,3.13,0,0,1,.62,2,3.71,3.71,0,0,1-.47,1.95,4,4,0,0,1-1.23,1.3,4.78,4.78,0,0,1-1.67.7,8.91,8.91,0,0,1-1.83.2h-7Z"/></svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -0,0 +1 @@
<svg viewBox="0 0 144.8 144.8" xmlns="http://www.w3.org/2000/svg"><circle cx="72.4" cy="72.4" r="72.4" fill="#fff"/><path d="M29.5,111.8c10.6,11.6,25.9,18.8,42.9,18.8c8.7,0,16.9-1.9,24.3-5.3L56.3,85L29.5,111.8z" fill="#ED2224"/><path d="m56.1 60.6l-30.6 30.5-4.1-4.1 32.2-32.2 37.6-37.6c-5.9-2-12.2-3.1-18.8-3.1-32.2 0-58.3 26.1-58.3 58.3 0 13.1 4.3 25.2 11.7 35l30.5-30.5 2.1 2 43.7 43.7c0.9-0.5 1.7-1 2.5-1.6l-48.3-48.3-29.3 29.3-4.1-4.1 33.4-33.4 2.1 2 51 50.9c0.8-0.6 1.5-1.3 2.2-1.9l-55-55-0.5 0.1z" fill="#ED2224"/><path d="m115.7 111.4c9.3-10.3 15-24 15-39 0-23.4-13.8-43.5-33.6-52.8l-36.7 36.6 55.3 55.2zm-41.2-44.6l-4.1-4.1 28.9-28.9 4.1 4.1-28.9 28.9zm27.4-39.7l-33.3 33.3-4.1-4.1 33.3-33.3 4.1 4.1z" fill="#ED1C24"/><path d="m72.4 144.8c-39.9 0-72.4-32.5-72.4-72.4s32.5-72.4 72.4-72.4 72.4 32.5 72.4 72.4-32.5 72.4-72.4 72.4zm0-137.5c-35.9 0-65.1 29.2-65.1 65.1s29.2 65.1 65.1 65.1 65.1-29.2 65.1-65.1-29.2-65.1-65.1-65.1z" fill="#ED2224"/></svg>

After

Width:  |  Height:  |  Size: 957 B

View File

@@ -265,18 +265,24 @@ export function parseConfigIdFromLabel(label: string): string | null {
}
/**
* Find collection by config ID in Plex collections using labels as fallback
* First tries to match by ratingKey, then falls back to matching by label
* Find collection by config ID in Plex collections using multiple matching strategies
* 1. First tries to match by ratingKey (fastest)
* 2. Falls back to matching by config ID in labels
* 3. Final fallback: exact name matching (only for Agregarr-labeled collections)
*/
export function findCollectionByConfigId(
configId: string,
ratingKey: string | undefined,
allCollections: {
ratingKey: string;
title?: string;
libraryKey?: string;
labels?: (string | { tag: string })[];
}[],
configType?: string,
configSubtype?: string
configSubtype?: string,
configName?: string,
configLibraryId?: string
): boolean {
// Use already imported logger
@@ -358,6 +364,82 @@ export function findCollectionByConfigId(
return hasMatchingLabel;
});
// Third fallback: name matching for Agregarr-labeled collections (only if label matching failed)
if (!foundByLabel && configName && configLibraryId) {
const matchingCollections = allCollections.filter((collection) => {
// Must be in the same library
if (
collection.libraryKey &&
String(collection.libraryKey) !== String(configLibraryId)
) {
return false;
}
// Must have a title to match against
if (!collection.title) return false;
// Try exact name match first
if (collection.title === configName) return true;
// Try fuzzy matching for common variations
const normalizedConfigName = configName.toLowerCase().trim();
const normalizedCollectionTitle = collection.title.toLowerCase().trim();
return normalizedConfigName === normalizedCollectionTitle;
});
if (matchingCollections.length === 0) {
// No name matches found - this is expected and not an error
return false;
}
if (matchingCollections.length > 1) {
logger.warn(
`Multiple Plex collections found matching config name "${configName}" - skipping name match for safety`,
{
label: 'Collection Matching',
configId,
configName,
matchingTitles: matchingCollections.map((c) => c.title),
matchingRatingKeys: matchingCollections.map((c) => c.ratingKey),
}
);
return false;
}
// Single match found
const matchingCollection = matchingCollections[0];
// Check if this collection has Agregarr labels (indicates it was managed by us)
const hasAgregarrLabels = matchingCollection.labels?.some((label) => {
const labelText = typeof label === 'string' ? label : label.tag;
return labelText.toLowerCase().startsWith('agregarr');
});
// Only proceed if it has Agregarr labels (safety check to avoid matching unrelated collections)
if (hasAgregarrLabels) {
logger.debug(`Collection found by name match: ${configId}`, {
label: 'Collection Matching',
configId,
configName,
collectionTitle: matchingCollection.title,
collectionRatingKey: matchingCollection.ratingKey,
});
return true;
} else {
logger.debug(
`Name match found but collection lacks Agregarr labels - skipping for safety: ${configName}`,
{
label: 'Collection Matching',
configId,
configName,
collectionTitle: matchingCollection.title,
collectionRatingKey: matchingCollection.ratingKey,
}
);
return false;
}
}
if (!foundByLabel) {
logger.debug(`Collection not found: ${configId}`, {
label: 'Collection Matching',
@@ -411,41 +493,102 @@ export async function syncConfigsWithPlexCollections(
for (const config of collectionConfigs) {
try {
// Skip if config already has correct rating key and we can find it in Plex
if (config.collectionRatingKey) {
const existsWithCorrectKey = allCollections.some(
(c) => c.ratingKey === config.collectionRatingKey
);
if (existsWithCorrectKey) {
logger.debug(`Config ${config.id} already has correct rating key`, {
label: 'Collection Config Sync',
configId: config.id,
ratingKey: config.collectionRatingKey,
});
continue;
}
}
// Always check and sync labels even if rating key exists - ensures labels are always correct
// Find Plex collection by name matching within the same library
const matchingCollections = allCollections.filter((collection) => {
// CRITICAL: Must be in the same library
// Try to find collection using same logic as findCollectionByConfigId for consistency
// First, try label parsing (same as discovery validation)
let matchingCollection: (typeof allCollections)[0] | null = null;
for (const collection of allCollections) {
// Must be in same library
if (
collection.libraryKey &&
String(collection.libraryKey) !== String(config.libraryId)
) {
return false;
continue;
}
// Try exact name match first
if (collection.title === config.name) return true;
// Try label parsing match first (consistent with discovery)
if (collection.labels) {
const hasMatchingLabel = collection.labels.some((label) => {
const labelText = typeof label === 'string' ? label : label.tag;
const parsedConfigId = parseConfigIdFromLabel(labelText);
return parsedConfigId === config.id;
});
// Try fuzzy matching for common variations
const normalizedConfigName = config.name.toLowerCase().trim();
const normalizedCollectionTitle = collection.title.toLowerCase().trim();
return normalizedConfigName === normalizedCollectionTitle;
});
if (hasMatchingLabel) {
matchingCollection = collection;
logger.debug(
`Config sync found collection by label: ${config.id}`,
{
label: 'Collection Config Sync',
configId: config.id,
configName: config.name,
collectionTitle: collection.title,
collectionRatingKey: collection.ratingKey,
}
);
break;
}
}
}
if (matchingCollections.length === 0) {
// Fallback: Try name matching (only if label parsing failed)
if (!matchingCollection) {
const matchingCollections = allCollections.filter((collection) => {
// CRITICAL: Must be in the same library
if (
collection.libraryKey &&
String(collection.libraryKey) !== String(config.libraryId)
) {
return false;
}
// Must have Agregarr labels (safety check - consistent with discovery)
const hasAgregarrLabels = collection.labels?.some((label) => {
const labelText = typeof label === 'string' ? label : label.tag;
return labelText.toLowerCase().startsWith('agregarr');
});
if (!hasAgregarrLabels) return false;
// Try exact name match first
if (collection.title === config.name) return true;
// Try fuzzy matching for common variations
const normalizedConfigName = config.name.toLowerCase().trim();
const normalizedCollectionTitle = collection.title
.toLowerCase()
.trim();
return normalizedConfigName === normalizedCollectionTitle;
});
if (matchingCollections.length === 1) {
matchingCollection = matchingCollections[0];
logger.debug(`Config sync found collection by name: ${config.id}`, {
label: 'Collection Config Sync',
configId: config.id,
configName: config.name,
collectionTitle: matchingCollection.title,
collectionRatingKey: matchingCollection.ratingKey,
});
} else if (matchingCollections.length > 1) {
logger.warn(
`Multiple Plex collections found matching config "${config.name}" - skipping`,
{
label: 'Collection Config Sync',
configId: config.id,
configName: config.name,
matchingTitles: matchingCollections.map((c) => c.title),
}
);
continue;
}
}
// If no collection found by either method, skip this config
if (!matchingCollection) {
logger.debug(
`No Plex collection found matching config "${config.name}"`,
{
@@ -457,28 +600,29 @@ export async function syncConfigsWithPlexCollections(
continue;
}
if (matchingCollections.length > 1) {
logger.warn(
`Multiple Plex collections found matching config "${config.name}" - skipping`,
{
label: 'Collection Config Sync',
configId: config.id,
configName: config.name,
matchingTitles: matchingCollections.map((c) => c.title),
}
);
continue;
}
const matchingCollection = matchingCollections[0];
// Check if this collection has an Agregarr label already
// Check if this collection has an Agregarr label already (safety check)
const existingAgregarrLabels =
matchingCollection.labels?.filter((label) => {
const labelText = typeof label === 'string' ? label : label.tag;
return labelText.toLowerCase().startsWith('agregarr');
}) || [];
// Safety check: Only sync collections that already have Agregarr labels
// This prevents accidentally taking over unrelated user collections
if (existingAgregarrLabels.length === 0) {
logger.debug(
`Skipping collection "${matchingCollection.title}" - no existing Agregarr labels found (safety check)`,
{
label: 'Collection Config Sync',
configId: config.id,
configName: config.name,
collectionTitle: matchingCollection.title,
collectionRatingKey: matchingCollection.ratingKey,
}
);
continue;
}
// Generate the correct label for our config
const correctLabel = createCollectionLabel(
config.source as CollectionSource,
@@ -493,30 +637,19 @@ export async function syncConfigsWithPlexCollections(
);
updatedPlexLabels.push(matchingCollection.ratingKey);
if (existingAgregarrLabels.length === 0) {
logger.info(
`Added label "${correctLabel}" to collection "${matchingCollection.title}"`,
{
label: 'Collection Config Sync',
configId: config.id,
ratingKey: matchingCollection.ratingKey,
addedLabel: correctLabel,
}
);
} else {
logger.info(
`Replaced labels on collection "${matchingCollection.title}" with "${correctLabel}"`,
{
label: 'Collection Config Sync',
configId: config.id,
ratingKey: matchingCollection.ratingKey,
oldLabels: existingAgregarrLabels.map((l) =>
typeof l === 'string' ? l : l.tag
),
newLabel: correctLabel,
}
);
}
// Since we now require existing Agregarr labels, we're always replacing them
logger.info(
`Replaced labels on collection "${matchingCollection.title}" with "${correctLabel}"`,
{
label: 'Collection Config Sync',
configId: config.id,
ratingKey: matchingCollection.ratingKey,
oldLabels: existingAgregarrLabels.map((l) =>
typeof l === 'string' ? l : l.tag
),
newLabel: correctLabel,
}
);
// Note: We don't update the config here since it's a copy
// Return the sync info so the caller can update the actual settings

View File

@@ -281,15 +281,30 @@ export class DiscoveryService {
);
}
// Add discovered pre-existing configs to settings
// Add discovered pre-existing configs to settings while preserving missing flags
if (discoveredPreExistingConfigs.length > 0) {
const existingPreExistingConfigs =
settings.plex.preExistingCollectionConfigs || [];
// Create map of existing configs with their missing flags
const existingConfigsMap = new Map(
existingPreExistingConfigs.map((config) => [config.id, config])
);
const newPreExistingConfigs = [...existingPreExistingConfigs];
for (const discoveredPreExisting of discoveredPreExistingConfigs) {
// Add isActive: true to make it a complete PreExistingCollectionConfig
const finalConfig = { ...discoveredPreExisting, isActive: true };
// Preserve missing flag if config already exists
const existingConfig = existingConfigsMap.get(
discoveredPreExisting.id
);
const finalConfig = {
...discoveredPreExisting,
isActive: true,
...(existingConfig?.missing !== undefined && {
missing: existingConfig.missing,
}),
};
newPreExistingConfigs.push(finalConfig);
}
@@ -317,7 +332,15 @@ export class DiscoveryService {
);
if (existingIndex !== -1) {
currentPreExistingConfigs[existingIndex] = enhancedConfig;
// Preserve missing flag when updating with enhanced data
const existingMissing =
currentPreExistingConfigs[existingIndex].missing;
currentPreExistingConfigs[existingIndex] = {
...enhancedConfig,
...(existingMissing !== undefined && {
missing: existingMissing,
}),
};
}
}
@@ -477,7 +500,9 @@ export class DiscoveryService {
config.collectionRatingKey,
allCollections,
config.type,
config.subtype
config.subtype,
config.name,
config.libraryId
);
if (!collectionExists) {

View File

@@ -508,7 +508,11 @@ collectionsRoutes.delete(
return !shouldRemove;
});
// Remove missing pre-existing collections
// Remove missing pre-existing collections and delete from hubs
const missingPreExisting = (
settings.plex.preExistingCollectionConfigs || []
).filter((config) => config.missing === true);
const filteredPreExisting = (
settings.plex.preExistingCollectionConfigs || []
).filter((config) => {
@@ -527,6 +531,41 @@ collectionsRoutes.delete(
return !shouldRemove;
});
// Delete missing pre-existing collections from Plex hubs
if (plexClient && missingPreExisting.length > 0) {
for (const config of missingPreExisting) {
if (config.collectionRatingKey && config.libraryId) {
try {
// Generate the hub identifier for pre-existing collections
const hubIdentifier = `custom.collection.${config.libraryId}.${config.collectionRatingKey}`;
await plexClient.deleteHubItem(config.libraryId, hubIdentifier);
hubDeleteCount++;
logger.info(
`Deleted missing pre-existing collection from Plex hub: ${config.name}`,
{
label: 'Collections API - Cleanup',
configId: config.id,
hubIdentifier,
libraryId: config.libraryId,
ratingKey: config.collectionRatingKey,
}
);
} catch (error) {
logger.warn(
`Failed to delete pre-existing collection from Plex hub: ${config.name}`,
{
label: 'Collections API - Cleanup',
configId: config.id,
error: error instanceof Error ? error.message : String(error),
}
);
}
}
}
}
// Update settings with filtered configs
settings.plex.collectionConfigs = filteredCollections;
settings.plex.hubConfigs = filteredHubs;