Skip to content

Commit

Permalink
Limit unwatched to a single season if required
Browse files Browse the repository at this point in the history
  • Loading branch information
s-t-e-v-e-n-k committed Aug 20, 2023
1 parent abea16f commit 85dec0f
Show file tree
Hide file tree
Showing 4 changed files with 255 additions and 12 deletions.
25 changes: 19 additions & 6 deletions jellyash/unwatched.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from operator import attrgetter
from typing import Optional

from .cli import argparse_parser
from .client import authed_client
Expand All @@ -13,11 +14,14 @@ def unwatched() -> None:
client = authed_client()
parser = argparse_parser()
parser.add_argument("show", nargs="*")
parser.add_argument("-s", "--season", type=int)
args = parser.parse_args()
if args.season and not args.show:
parser.error("Need to specify a show when specifiying a season")
if not args.show:
all_unwatched(client)
else:
specific_unwatched(client, " ".join(args.show))
specific_unwatched(client, " ".join(args.show), season=args.season)


def all_unwatched(client) -> None:
Expand All @@ -32,14 +36,23 @@ def all_unwatched(client) -> None:
print(f"Total: {pluralized_str(total)}")


def specific_unwatched(client, term: str) -> None:
def specific_unwatched(client, term: str, season: Optional[int]) -> None:
try:
show = search_single_show(client, term)
except ValueError as e:
print(str(e))
return
seasons = client.jellyfin.get_seasons(show.Id)
name = f"{show.Name}"
if season:
show = next((s for s in seasons if s.IndexNumber == season), None)
if show is None:
print(f"Can not find season {season} of {name}")
return
name += f", Season {season}"
total = show.ChildCount
else:
total = sum(s.ChildCount for s in seasons)
unwatched = show.UserData.UnplayedItemCount
total = sum(s.ChildCount for s in client.jellyfin.get_seasons(show.Id))
count = total - unwatched
print(f"{show.Name}: {pluralized_str(count, prefix='')}")
print(f"{show.Name}: {pluralized_str(unwatched)}")
print(f"{name}: {pluralized_str(total - unwatched, prefix='')}")
print(f"{name}: {pluralized_str(unwatched)}")
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
interactions:
- request:
body: null
headers:
Accept:
- '*/*'
Accept-Charset:
- UTF-8,*
Accept-encoding:
- gzip
Connection:
- keep-alive
Content-type:
- application/json
User-Agent:
- jellyash_test_testunwatched/0.5.1
X-MediaBrowser-Token:
- 077919a559e04094896846f622257594
x-emby-authorization:
- MediaBrowser Client=jellyash_test_testunwatched, Device=wrecked, DeviceId=6aa06a0d-6af8-4545-a99b-ea512901c041,
Version=0.5.1, UserId=a076a5bfc9034f379f5889bc6dafc77b
method: GET
uri: https://demo.jellyfin.org/stable/Users/a076a5bfc9034f379f5889bc6dafc77b/Items?searchTerm=Pioneer+One&Recursive=True&IncludeItemTypes=Series&Limit=20
response:
body:
string: '{"Items":[{"Name":"Pioneer One","ServerId":"713dc3fe952b438fa70ed35e4ef0525a","Id":"05991932707d2c668148d8ed19cdb544","PremiereDate":"2010-06-16T00:00:00.0000000Z","OfficialRating":"NR","ChannelId":null,"CommunityRating":6.9,"RunTimeTicks":17999998976,"ProductionYear":2010,"IsFolder":true,"Type":"Series","UserData":{"UnplayedItemCount":1,"PlaybackPositionTicks":0,"PlayCount":0,"IsFavorite":false,"Played":false,"Key":"170551"},"Status":"Ended","AirDays":[],"ImageTags":{"Primary":"63bd1ece25c9fdb2d91b6b4d48ae07f1","Banner":"f06d9e317cb59d0f050e2fbd1fe64e54"},"BackdropImageTags":["b8f1332f5ca6e72d8129c4bceadd100b"],"ImageBlurHashes":{"Backdrop":{"b8f1332f5ca6e72d8129c4bceadd100b":"WiI4hd}q,--=x]t6~WrqZ~xvtRoeIpD*IUjFniaen%aeafjFaLV["},"Primary":{"63bd1ece25c9fdb2d91b6b4d48ae07f1":"drKJ[S-pX8M{~BxaX8M{8^RjWXNGi^RjRjog-;RkRjt7"},"Banner":{"f06d9e317cb59d0f050e2fbd1fe64e54":"HlH_S*=yrC$ext%MtRtRWn~WxZeTs8s.t7ozofWB"}},"LocationType":"FileSystem","EndDate":"2011-12-13T00:00:00.0000000Z"}],"TotalRecordCount":1,"StartIndex":0}'
headers:
content-type:
- application/json; charset=utf-8
date:
- Fri, 18 Aug 2023 04:44:49 GMT
referrer-policy:
- no-referrer,same-origin,strict-origin,strict-origin-when-cross-origin
server:
- Kestrel
strict-transport-security:
- max-age=31536000;includeSubDomains;preload
transfer-encoding:
- chunked
x-content-type-options:
- nosniff
x-response-time-ms:
- '8'
x-xss-protection:
- 1;mode=block
status:
code: 200
message: OK
- request:
body: null
headers:
Accept:
- '*/*'
Accept-Charset:
- UTF-8,*
Accept-encoding:
- gzip
Connection:
- keep-alive
Content-type:
- application/json
User-Agent:
- jellyash_test_testunwatched/0.5.1
X-MediaBrowser-Token:
- 077919a559e04094896846f622257594
x-emby-authorization:
- MediaBrowser Client=jellyash_test_testunwatched, Device=wrecked, DeviceId=6aa06a0d-6af8-4545-a99b-ea512901c041,
Version=0.5.1, UserId=a076a5bfc9034f379f5889bc6dafc77b
method: GET
uri: https://demo.jellyfin.org/stable/Shows/05991932707d2c668148d8ed19cdb544/Seasons?UserId=a076a5bfc9034f379f5889bc6dafc77b&EnableImages=True&Fields=Path%2CGenres%2CSortName%2CStudios%2CWriter%2CTaglines%2CLocalTrailerCount%2COfficialRating%2CCumulativeRunTimeTicks%2CItemCounts%2CMetascore%2CAirTime%2CDateCreated%2CPeople%2COverview%2CCriticRating%2CCriticRatingSummary%2CEtag%2CShortOverview%2CProductionLocations%2CTags%2CProviderIds%2CParentId%2CRemoteTrailers%2CSpecialEpisodeNumbers%2CMediaSources%2CVoteCount%2CRecursiveItemCount%2CPrimaryImageAspectRatio
response:
body:
string: '{"Items":[{"Name":"Season 1","ServerId":"713dc3fe952b438fa70ed35e4ef0525a","Id":"0bf82b6ecd61047b8dea0eb0035ad364","Etag":"7bbc26ed572a7ca4ca5ca2278c4b59dd","DateCreated":"2019-11-29T03:46:20.9183807Z","SortName":"0001","PremiereDate":"2010-06-16T00:00:00.0000000Z","Path":"/media/TV
Shows/Pioneer One (2010)/Season 01","ChannelId":null,"Taglines":[],"Genres":[],"ProductionYear":2010,"IndexNumber":1,"RemoteTrailers":[],"ProviderIds":{},"IsFolder":true,"ParentId":"05991932707d2c668148d8ed19cdb544","Type":"Season","People":[],"Studios":[],"GenreItems":[],"ParentBackdropItemId":"05991932707d2c668148d8ed19cdb544","ParentBackdropImageTags":["b8f1332f5ca6e72d8129c4bceadd100b"],"LocalTrailerCount":0,"UserData":{"PlayedPercentage":50,"UnplayedItemCount":1,"PlaybackPositionTicks":0,"PlayCount":0,"IsFavorite":false,"Played":false,"Key":"170551001"},"RecursiveItemCount":2,"ChildCount":2,"SeriesName":"Pioneer
One","SeriesId":"05991932707d2c668148d8ed19cdb544","Tags":[],"PrimaryImageAspectRatio":0.6920415224913494,"SeriesPrimaryImageTag":"63bd1ece25c9fdb2d91b6b4d48ae07f1","ImageTags":{"Primary":"792441701f42e7ddbd69888686eae67c"},"BackdropImageTags":[],"ImageBlurHashes":{"Primary":{"792441701f42e7ddbd69888686eae67c":"djMYf^^\u002BODRj=Gs,XSR\u002B0LR5WBR\u002BxuWEVta{OsWYsAba","63bd1ece25c9fdb2d91b6b4d48ae07f1":"drKJ[S-pX8M{~BxaX8M{8^RjWXNGi^RjRjog-;RkRjt7"},"Backdrop":{"b8f1332f5ca6e72d8129c4bceadd100b":"WiI4hd}q,--=x]t6~WrqZ~xvtRoeIpD*IUjFniaen%aeafjFaLV["}},"LocationType":"FileSystem"}],"TotalRecordCount":1,"StartIndex":0}'
headers:
content-type:
- application/json; charset=utf-8
date:
- Fri, 18 Aug 2023 04:44:50 GMT
referrer-policy:
- no-referrer,same-origin,strict-origin,strict-origin-when-cross-origin
server:
- Kestrel
strict-transport-security:
- max-age=31536000;includeSubDomains;preload
transfer-encoding:
- chunked
x-content-type-options:
- nosniff
x-response-time-ms:
- '17'
x-xss-protection:
- 1;mode=block
status:
code: 200
message: OK
version: 1
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
interactions:
- request:
body: null
headers:
Accept:
- '*/*'
Accept-Charset:
- UTF-8,*
Accept-encoding:
- gzip
Connection:
- keep-alive
Content-type:
- application/json
User-Agent:
- jellyash_test_testunwatched/0.5.1
X-MediaBrowser-Token:
- 40e740700633466fa55f2bb26e75f39b
x-emby-authorization:
- MediaBrowser Client=jellyash_test_testunwatched, Device=wrecked, DeviceId=f99e0d17-e561-44d9-a17c-f1e3324135f0,
Version=0.5.1, UserId=a076a5bfc9034f379f5889bc6dafc77b
method: GET
uri: https://demo.jellyfin.org/stable/Users/a076a5bfc9034f379f5889bc6dafc77b/Items?searchTerm=Pioneer+One&Recursive=True&IncludeItemTypes=Series&Limit=20
response:
body:
string: '{"Items":[{"Name":"Pioneer One","ServerId":"713dc3fe952b438fa70ed35e4ef0525a","Id":"05991932707d2c668148d8ed19cdb544","PremiereDate":"2010-06-16T00:00:00.0000000Z","OfficialRating":"NR","ChannelId":null,"CommunityRating":6.9,"RunTimeTicks":17999998976,"ProductionYear":2010,"IsFolder":true,"Type":"Series","UserData":{"UnplayedItemCount":1,"PlaybackPositionTicks":0,"PlayCount":0,"IsFavorite":false,"Played":false,"Key":"170551"},"Status":"Ended","AirDays":[],"ImageTags":{"Primary":"63bd1ece25c9fdb2d91b6b4d48ae07f1","Banner":"f06d9e317cb59d0f050e2fbd1fe64e54"},"BackdropImageTags":["b8f1332f5ca6e72d8129c4bceadd100b"],"ImageBlurHashes":{"Backdrop":{"b8f1332f5ca6e72d8129c4bceadd100b":"WiI4hd}q,--=x]t6~WrqZ~xvtRoeIpD*IUjFniaen%aeafjFaLV["},"Primary":{"63bd1ece25c9fdb2d91b6b4d48ae07f1":"drKJ[S-pX8M{~BxaX8M{8^RjWXNGi^RjRjog-;RkRjt7"},"Banner":{"f06d9e317cb59d0f050e2fbd1fe64e54":"HlH_S*=yrC$ext%MtRtRWn~WxZeTs8s.t7ozofWB"}},"LocationType":"FileSystem","EndDate":"2011-12-13T00:00:00.0000000Z"}],"TotalRecordCount":1,"StartIndex":0}'
headers:
content-type:
- application/json; charset=utf-8
date:
- Fri, 18 Aug 2023 04:47:49 GMT
referrer-policy:
- no-referrer,same-origin,strict-origin,strict-origin-when-cross-origin
server:
- Kestrel
strict-transport-security:
- max-age=31536000;includeSubDomains;preload
transfer-encoding:
- chunked
x-content-type-options:
- nosniff
x-response-time-ms:
- '5'
x-xss-protection:
- 1;mode=block
status:
code: 200
message: OK
- request:
body: null
headers:
Accept:
- '*/*'
Accept-Charset:
- UTF-8,*
Accept-encoding:
- gzip
Connection:
- keep-alive
Content-type:
- application/json
User-Agent:
- jellyash_test_testunwatched/0.5.1
X-MediaBrowser-Token:
- 40e740700633466fa55f2bb26e75f39b
x-emby-authorization:
- MediaBrowser Client=jellyash_test_testunwatched, Device=wrecked, DeviceId=f99e0d17-e561-44d9-a17c-f1e3324135f0,
Version=0.5.1, UserId=a076a5bfc9034f379f5889bc6dafc77b
method: GET
uri: https://demo.jellyfin.org/stable/Shows/05991932707d2c668148d8ed19cdb544/Seasons?UserId=a076a5bfc9034f379f5889bc6dafc77b&EnableImages=True&Fields=Path%2CGenres%2CSortName%2CStudios%2CWriter%2CTaglines%2CLocalTrailerCount%2COfficialRating%2CCumulativeRunTimeTicks%2CItemCounts%2CMetascore%2CAirTime%2CDateCreated%2CPeople%2COverview%2CCriticRating%2CCriticRatingSummary%2CEtag%2CShortOverview%2CProductionLocations%2CTags%2CProviderIds%2CParentId%2CRemoteTrailers%2CSpecialEpisodeNumbers%2CMediaSources%2CVoteCount%2CRecursiveItemCount%2CPrimaryImageAspectRatio
response:
body:
string: '{"Items":[{"Name":"Season 1","ServerId":"713dc3fe952b438fa70ed35e4ef0525a","Id":"0bf82b6ecd61047b8dea0eb0035ad364","Etag":"7bbc26ed572a7ca4ca5ca2278c4b59dd","DateCreated":"2019-11-29T03:46:20.9183807Z","SortName":"0001","PremiereDate":"2010-06-16T00:00:00.0000000Z","Path":"/media/TV
Shows/Pioneer One (2010)/Season 01","ChannelId":null,"Taglines":[],"Genres":[],"ProductionYear":2010,"IndexNumber":1,"RemoteTrailers":[],"ProviderIds":{},"IsFolder":true,"ParentId":"05991932707d2c668148d8ed19cdb544","Type":"Season","People":[],"Studios":[],"GenreItems":[],"ParentBackdropItemId":"05991932707d2c668148d8ed19cdb544","ParentBackdropImageTags":["b8f1332f5ca6e72d8129c4bceadd100b"],"LocalTrailerCount":0,"UserData":{"PlayedPercentage":50,"UnplayedItemCount":1,"PlaybackPositionTicks":0,"PlayCount":0,"IsFavorite":false,"Played":false,"Key":"170551001"},"RecursiveItemCount":2,"ChildCount":2,"SeriesName":"Pioneer
One","SeriesId":"05991932707d2c668148d8ed19cdb544","Tags":[],"PrimaryImageAspectRatio":0.6920415224913494,"SeriesPrimaryImageTag":"63bd1ece25c9fdb2d91b6b4d48ae07f1","ImageTags":{"Primary":"792441701f42e7ddbd69888686eae67c"},"BackdropImageTags":[],"ImageBlurHashes":{"Primary":{"792441701f42e7ddbd69888686eae67c":"djMYf^^\u002BODRj=Gs,XSR\u002B0LR5WBR\u002BxuWEVta{OsWYsAba","63bd1ece25c9fdb2d91b6b4d48ae07f1":"drKJ[S-pX8M{~BxaX8M{8^RjWXNGi^RjRjog-;RkRjt7"},"Backdrop":{"b8f1332f5ca6e72d8129c4bceadd100b":"WiI4hd}q,--=x]t6~WrqZ~xvtRoeIpD*IUjFniaen%aeafjFaLV["}},"LocationType":"FileSystem"}],"TotalRecordCount":1,"StartIndex":0}'
headers:
content-type:
- application/json; charset=utf-8
date:
- Fri, 18 Aug 2023 04:47:50 GMT
referrer-policy:
- no-referrer,same-origin,strict-origin,strict-origin-when-cross-origin
server:
- Kestrel
strict-transport-security:
- max-age=31536000;includeSubDomains;preload
transfer-encoding:
- chunked
x-content-type-options:
- nosniff
x-response-time-ms:
- '7'
x-xss-protection:
- 1;mode=block
status:
code: 200
message: OK
version: 1
46 changes: 40 additions & 6 deletions tests/test_unwatched.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,20 +43,54 @@ def test_all_unwatched(self):

@pytest.mark.vcr
def test_specific_unwatched(self):
specific_unwatched(self.test_client, "Pioneer One")
specific_unwatched(self.test_client, self.show, None)
captured = self.capsys.readouterr()
wstr = "watched episode\n"
expected = f"{self.show}: 1 {wstr}{self.show}: 1 un{wstr}"
self.assertEqual(captured.out, expected)
self.assertEqual(captured.err, "")

@pytest.mark.vcr
def test_specific_unwatched_with_season(self):
specific_unwatched(self.test_client, self.show, 1)
captured = self.capsys.readouterr()
name = f"{self.show}, Season 1"
wstr = "watched episode\n"
expected = f"{name}: 1 {wstr}{name}: 1 un{wstr}"
self.assertEqual(captured.out, expected)
self.assertEqual(captured.err, "")

@pytest.mark.vcr
def test_specific_unwatched_unknown_season(self):
specific_unwatched(self.test_client, self.show, 3)
captured = self.capsys.readouterr()
expected = "Can not find season 3 of Pioneer One\n"
self.assertEqual(captured.out, expected)
self.assertEqual(captured.err, "")

@pytest.mark.vcr
def test_specific_unwatched_not_found(self):
specific_unwatched(self.test_client, "NotFound")
specific_unwatched(self.test_client, "NotFound", None)
captured = self.capsys.readouterr()
self.assertEqual(captured.out, "NotFound not found\n")
self.assertEqual(captured.err, "")

@pytest.mark.block_network
def test_unwatched_season_without_show(self):
with patch("jellyash.unwatched.authed_client"):
with patch("argparse.ArgumentParser.parse_args",
return_value=argparse.Namespace(show=[], season=3)
):
# Sigh, parser.error will always exit.
with self.assertRaises(SystemExit):
unwatched()
captured = self.capsys.readouterr()
self.assertEqual(captured.out, "")
self.assertIn(
"Need to specify a show when specifiying a season",
captured.err
)


class TestUnwatchedIntegration(unittest.TestCase):
@pytest.mark.block_network
Expand All @@ -65,22 +99,22 @@ def test_unwatched_all(self):
with patch("jellyash.unwatched.all_unwatched") as all_mock:
with patch(
"argparse.ArgumentParser.parse_args",
return_value=argparse.Namespace(show=[])
return_value=argparse.Namespace(show=[], season=None)
):
unwatched()
all_mock.assert_called_once_with(client_mock())

@pytest.mark.block_network
def test_unwatched_specific(self):
namespace = argparse.Namespace(show=["Foo", "Bar"], season=None)
with patch("jellyash.unwatched.authed_client") as client_mock:
with patch(
"jellyash.unwatched.specific_unwatched") as specific_mock:
with patch(
"argparse.ArgumentParser.parse_args",
return_value=argparse.Namespace(show=["Foo", "Bar"])
return_value=namespace
):
unwatched()
specific_mock.assert_called_once_with(
client_mock(), "Foo Bar"
client_mock(), "Foo Bar", season=None
)

0 comments on commit 85dec0f

Please sign in to comment.