Skip to content

Commit 7d9eb11

Browse files
tablackburnclaude
andauthored
feat: add extended media details to Search-PatMedia output (#23)
Add 11 new properties to search results with no performance impact: - Duration (milliseconds) and DurationFormatted (human-readable) - ContentRating (PG-13, R, TV-MA, etc.) - Rating and AudienceRating (critic/audience scores) - Studio (production company) - ViewCount (watch history) - OriginallyAvailableAt (release date) - ShowName, Season, Episode (for TV episodes) All fields are sourced from existing search API response data. Also adds Format-PatDuration private helper for duration formatting. Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 3f92a6a commit 7d9eb11

4 files changed

Lines changed: 431 additions & 48 deletions

File tree

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
function Format-PatDuration {
2+
<#
3+
.SYNOPSIS
4+
Formats a duration in milliseconds as a human-readable string.
5+
6+
.DESCRIPTION
7+
Internal helper function that converts a duration in milliseconds to a human-readable
8+
string showing hours and minutes (e.g., "2h 16m") or just minutes for shorter durations.
9+
10+
.PARAMETER Milliseconds
11+
The duration in milliseconds to format.
12+
13+
.OUTPUTS
14+
System.String
15+
Returns a formatted string representing the duration (e.g., "2h 16m", "45m", "0m").
16+
Returns $null if input is $null or 0.
17+
18+
.EXAMPLE
19+
Format-PatDuration -Milliseconds 8160000
20+
Returns: "2h 16m"
21+
22+
.EXAMPLE
23+
Format-PatDuration -Milliseconds 2700000
24+
Returns: "45m"
25+
26+
.EXAMPLE
27+
Format-PatDuration -Milliseconds 0
28+
Returns: $null
29+
30+
.EXAMPLE
31+
8160000, 2700000, 5400000 | Format-PatDuration
32+
Returns: "2h 16m", "45m", "1h 30m"
33+
#>
34+
[CmdletBinding()]
35+
[OutputType([string])]
36+
param (
37+
[Parameter(Mandatory = $false, ValueFromPipeline = $true)]
38+
[AllowNull()]
39+
[long]
40+
$Milliseconds
41+
)
42+
43+
process {
44+
if ($null -eq $Milliseconds -or $Milliseconds -eq 0) {
45+
return $null
46+
}
47+
48+
$totalMinutes = [math]::Floor($Milliseconds / 60000)
49+
$hours = [math]::Floor($totalMinutes / 60)
50+
$minutes = $totalMinutes % 60
51+
52+
if ($hours -gt 0) {
53+
return "${hours}h ${minutes}m"
54+
}
55+
else {
56+
return "${minutes}m"
57+
}
58+
}
59+
}

PlexAutomationToolkit/Public/Search-PatMedia.ps1

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,17 @@ function Search-PatMedia {
7373
- LibraryId: ID of the library section containing this item
7474
- LibraryName: Name of the library section
7575
- ServerUri: URI of the Plex server
76+
- Duration: Duration in milliseconds (if applicable)
77+
- DurationFormatted: Human-readable duration (e.g., "2h 16m")
78+
- ContentRating: Age rating (e.g., "PG-13", "R", "TV-MA")
79+
- Rating: Critic/Plex rating (0-10)
80+
- AudienceRating: Audience rating (0-10)
81+
- Studio: Production company/studio
82+
- ViewCount: Number of times watched
83+
- OriginallyAvailableAt: Original release date
84+
- ShowName: TV show name (for episodes only)
85+
- Season: Season number (for episodes only)
86+
- Episode: Episode number (for episodes only)
7687
#>
7788
[CmdletBinding(DefaultParameterSetName = 'All')]
7889
[OutputType([PSCustomObject[]])]
@@ -83,7 +94,7 @@ function Search-PatMedia {
8394
$Query,
8495

8596
[Parameter(Mandatory = $false)]
86-
[string]
97+
[string]
8798
$ServerName,
8899

89100
[Parameter(Mandatory = $false)]
@@ -193,16 +204,27 @@ function Search-PatMedia {
193204
$itemLibraryName = if ($item.librarySectionTitle) { $item.librarySectionTitle } elseif ($resolvedSectionName) { $resolvedSectionName } else { $null }
194205

195206
[PSCustomObject]@{
196-
PSTypeName = 'PlexAutomationToolkit.SearchResult'
197-
Type = $hub.type
198-
RatingKey = if ($item.ratingKey) { [int]$item.ratingKey } else { $null }
199-
Title = $item.title
200-
Year = if ($item.year) { [int]$item.year } else { $null }
201-
Summary = $item.summary
202-
Thumb = $item.thumb
203-
LibraryId = $itemLibraryId
204-
LibraryName = $itemLibraryName
205-
ServerUri = $effectiveUri
207+
PSTypeName = 'PlexAutomationToolkit.SearchResult'
208+
Type = $hub.type
209+
RatingKey = if ($item.ratingKey) { [int]$item.ratingKey } else { $null }
210+
Title = $item.title
211+
Year = if ($item.year) { [int]$item.year } else { $null }
212+
Summary = $item.summary
213+
Thumb = $item.thumb
214+
LibraryId = $itemLibraryId
215+
LibraryName = $itemLibraryName
216+
ServerUri = $effectiveUri
217+
Duration = if ($item.duration) { [long]$item.duration } else { $null }
218+
DurationFormatted = Format-PatDuration -Milliseconds $item.duration
219+
ContentRating = $item.contentRating
220+
Rating = if ($item.rating) { [decimal]$item.rating } else { $null }
221+
AudienceRating = if ($item.audienceRating) { [decimal]$item.audienceRating } else { $null }
222+
Studio = $item.studio
223+
ViewCount = if ($item.viewCount) { [int]$item.viewCount } else { 0 }
224+
OriginallyAvailableAt = if ($item.originallyAvailableAt) { [datetime]$item.originallyAvailableAt } else { $null }
225+
ShowName = $item.grandparentTitle
226+
Season = if ($item.parentIndex) { [int]$item.parentIndex } else { $null }
227+
Episode = if ($item.index) { [int]$item.index } else { $null }
206228
}
207229
}
208230
}
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
BeforeAll {
2+
# Remove any loaded instances of the module to avoid "multiple modules" error
3+
Get-Module PlexAutomationToolkit -All | Remove-Module -Force -ErrorAction SilentlyContinue
4+
5+
# Import the module from the source directory for unit testing
6+
$modulePath = Join-Path -Path $PSScriptRoot -ChildPath '..\..\..\PlexAutomationToolkit\PlexAutomationToolkit.psm1'
7+
Import-Module $modulePath -Force
8+
}
9+
10+
Describe 'Format-PatDuration' {
11+
Context 'Hours and minutes formatting' {
12+
It 'Should format duration with hours and minutes' {
13+
InModuleScope PlexAutomationToolkit {
14+
$result = Format-PatDuration -Milliseconds 8160000
15+
$result | Should -Be '2h 16m'
16+
}
17+
}
18+
19+
It 'Should format 1 hour duration' {
20+
InModuleScope PlexAutomationToolkit {
21+
$result = Format-PatDuration -Milliseconds 3600000
22+
$result | Should -Be '1h 0m'
23+
}
24+
}
25+
26+
It 'Should format 1 hour 30 minutes' {
27+
InModuleScope PlexAutomationToolkit {
28+
$result = Format-PatDuration -Milliseconds 5400000
29+
$result | Should -Be '1h 30m'
30+
}
31+
}
32+
33+
It 'Should format long duration (3+ hours)' {
34+
InModuleScope PlexAutomationToolkit {
35+
$result = Format-PatDuration -Milliseconds 12600000
36+
$result | Should -Be '3h 30m'
37+
}
38+
}
39+
}
40+
41+
Context 'Minutes only formatting' {
42+
It 'Should format duration less than 1 hour' {
43+
InModuleScope PlexAutomationToolkit {
44+
$result = Format-PatDuration -Milliseconds 2700000
45+
$result | Should -Be '45m'
46+
}
47+
}
48+
49+
It 'Should format 30 minutes' {
50+
InModuleScope PlexAutomationToolkit {
51+
$result = Format-PatDuration -Milliseconds 1800000
52+
$result | Should -Be '30m'
53+
}
54+
}
55+
56+
It 'Should format 1 minute' {
57+
InModuleScope PlexAutomationToolkit {
58+
$result = Format-PatDuration -Milliseconds 60000
59+
$result | Should -Be '1m'
60+
}
61+
}
62+
63+
It 'Should format short duration (seconds only) as 0m' {
64+
InModuleScope PlexAutomationToolkit {
65+
$result = Format-PatDuration -Milliseconds 30000
66+
$result | Should -Be '0m'
67+
}
68+
}
69+
}
70+
71+
Context 'Null and zero handling' {
72+
It 'Should return null for zero milliseconds' {
73+
InModuleScope PlexAutomationToolkit {
74+
$result = Format-PatDuration -Milliseconds 0
75+
$result | Should -BeNullOrEmpty
76+
}
77+
}
78+
79+
It 'Should return null for null input' {
80+
InModuleScope PlexAutomationToolkit {
81+
$result = Format-PatDuration -Milliseconds $null
82+
$result | Should -BeNullOrEmpty
83+
}
84+
}
85+
}
86+
87+
Context 'Pipeline support' {
88+
It 'Should accept value from pipeline' {
89+
InModuleScope PlexAutomationToolkit {
90+
$result = 7200000 | Format-PatDuration
91+
$result | Should -Be '2h 0m'
92+
}
93+
}
94+
95+
It 'Should process multiple values from pipeline' {
96+
InModuleScope PlexAutomationToolkit {
97+
$results = @(8160000, 2700000, 5400000) | Format-PatDuration
98+
$results | Should -HaveCount 3
99+
$results[0] | Should -Be '2h 16m'
100+
$results[1] | Should -Be '45m'
101+
$results[2] | Should -Be '1h 30m'
102+
}
103+
}
104+
}
105+
106+
Context 'Output type' {
107+
It 'Should return a string' {
108+
InModuleScope PlexAutomationToolkit {
109+
$result = Format-PatDuration -Milliseconds 3600000
110+
$result | Should -BeOfType [string]
111+
}
112+
}
113+
}
114+
115+
Context 'Real-world media durations' {
116+
It 'Should format typical movie duration (2h 16m)' {
117+
InModuleScope PlexAutomationToolkit {
118+
# The Matrix: 136 minutes = 8160000ms
119+
$result = Format-PatDuration -Milliseconds 8160000
120+
$result | Should -Be '2h 16m'
121+
}
122+
}
123+
124+
It 'Should format typical TV episode duration (45m)' {
125+
InModuleScope PlexAutomationToolkit {
126+
# 45 minute episode = 2700000ms
127+
$result = Format-PatDuration -Milliseconds 2700000
128+
$result | Should -Be '45m'
129+
}
130+
}
131+
132+
It 'Should format typical sitcom duration (22m)' {
133+
InModuleScope PlexAutomationToolkit {
134+
# 22 minute sitcom = 1320000ms
135+
$result = Format-PatDuration -Milliseconds 1320000
136+
$result | Should -Be '22m'
137+
}
138+
}
139+
140+
It 'Should format extended edition movie (3h 21m)' {
141+
InModuleScope PlexAutomationToolkit {
142+
# LOTR Extended: 201 minutes = 12060000ms
143+
$result = Format-PatDuration -Milliseconds 12060000
144+
$result | Should -Be '3h 21m'
145+
}
146+
}
147+
}
148+
}

0 commit comments

Comments
 (0)