Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Podcast support #54

Open
wants to merge 21 commits into
base: master
Choose a base branch
from
Open

Podcast support #54

wants to merge 21 commits into from

Conversation

fracai
Copy link
Contributor

@fracai fracai commented Aug 28, 2021

Adds podcast support. This consists of treating tracks specially if they're found in the iPod_Control/Podcasts directory or if metadata contains "Podcast" in the genre list. Identified podcast tracks are excluded from the "All Songs" playlist and skipped when shuffling. In support of this, a Podcasts directory will be created when the script is run and symlinks will be followed to support creating playlists (this will aid regular music tracks as well).

@@ -408,7 +423,7 @@ def __init__(self, parent):
("header_id", ("4s", b"hphs")), #shph
("total_length", ("I", 0)),
("number_of_playlists", ("I", 0)),
("number_of_non_podcast_lists", ("2s", b"\xFF\xFF")),
("number_of_non_podcast_lists", ("H", 65535)),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did you change this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At the time, because I assumed I needed to specify that it was an unsigned short instead of two bytes, but I suppose they're equivalent. I do think it's useful to indicate the actual type now that it'll change values.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To reduce the noise I would revert this change

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just tested and actually can't revert this. Packing the structure to build the database throws an exception because the struct has a short but is expecting bytes.

Traceback (most recent call last):
  File ".../IPod-Shuffle-4g/./ipod-shuffle-4g.py", line 810, in <module>
    shuffle.write_database()
  File ".../IPod-Shuffle-4g/./ipod-shuffle-4g.py", line 675, in write_database
    f.write(self.tunessd.construct())
  File ".../IPod-Shuffle-4g/./ipod-shuffle-4g.py", line 293, in construct
    play_header = self.play_header.construct(self.track_header.tracks)
  File ".../IPod-Shuffle-4g/./ipod-shuffle-4g.py", line 463, in construct
    output = Record.construct(self)
  File ".../IPod-Shuffle-4g/./ipod-shuffle-4g.py", line 215, in construct
    output += struct.pack("<" + fmt, self._fields.get(i, default))
struct.error: argument for 's' must be a bytes object

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed H seems correct here. Can you give it the value 0xFFFF?

I am also wondering if a non-podcasts count makes sense. I guess a podcast count makes more sense, but that is what the docs from reverse engineering say. But maybe those are wrong!?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically, ("H", b"\xFF\xFF") would be accepted, but I think that just looks confusing, given that it's representing a number.

I also don't think I'm going to question the docs. If it was actually a "podcast count", 0 would make more sense for the case when there aren't any podcast playlists.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0xFFFF is more clear than the number. Maybe it is also a signed short, which would be -1.

@fracai
Copy link
Contributor Author

fracai commented Aug 29, 2021

I added readme mentions regarding podcast and symlink support.
And also marked podcast tracks as remembering their playback position.

README.md Outdated Show resolved Hide resolved
@aoss-doeice
Copy link

aoss-doeice commented Aug 31, 2021 via email

@NicoHood
Copy link
Collaborator

@aoss-doeice You have subscribed for the notifications yourself. Only you can unsubscribe - we cannot unsubscribe you.

Comment on lines 376 to 377
self["dontskip"] = 0
self["remember"] = 1
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please add more explanation what this does?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

update incoming

README.md Outdated
@@ -93,6 +93,9 @@ The file can be found in the [extras](extras) folder.
#### Compress/Convert your music files
([#11](https://github.com/nims11/IPod-Shuffle-4g/issues/11)) Shuffle is short on storage, and you might want to squeeze in more of your collection by sacrificing some bitrate off your files. In rarer cases, you might also possess music in formats not supported by your ipod. Although `ffmpeg` can handle almost all your needs, if you are looking for a friendly alternative, try [Soundconverter](http://soundconverter.org/).

#### Podcast support
Place podcast tracks in `iPod_Control/Podcasts` to generate playlists. These tracks will be skipped when shuffling, will be marked to remember their last playback position, and won't be included in the "All Songs" playlist.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or mark then as genre id3 tag "Podcast"?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

right, thanks

ipod-shuffle-4g.py Outdated Show resolved Hide resolved
ipod-shuffle-4g.py Outdated Show resolved Hide resolved
@@ -562,6 +589,9 @@ def construct(self, tracks):
for i in self.listtracks:
path = self.ipod_to_path(i)
position = -1
if self["listtype"] == 1 and "/iPod_Control/Podcasts/" in path:
print ('not including podcast in master playlist: {}'.format(path))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need to print that to the user? Also shouldnt the listtype be 3 instead of 1?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can remove that printout. The intention was to match the print level of other messages.
Also, this section is excluding podcasts from the All Songs playlist. That's a bit more clear with the enum, but I added a comment.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have not run the program for ages. If you say the print fits, we can keep it. But please use a capital Not at the beginning :-D

I currently try to understand the code again. Isnt there a track object inside that playlist of which we can check if it is a podcast. But at another point we also accept id3 tags with Genre set to Podcast, but this code here does not support this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not a track object, just a path to the track. In order to detect by id3 at this point, the code would have to reference the track header and look up each track object, or reload the metadata with mutagen. I think that would be better handled by a bigger re-architecture that loads all data, building the database objects and then writes everything at once, rather than building up some data as the database is built.

ipod-shuffle-4g.py Outdated Show resolved Hide resolved
ipod-shuffle-4g.py Outdated Show resolved Hide resolved
@@ -469,7 +469,7 @@ def construct(self, tracks):
playlist.populate(i)
construction = playlist.construct(tracks)
if playlist["number_of_songs"] > 0:
if playlist["listtype"] == 3:
if playlist["listtype"] == PlaylistType.PODCAST.value:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you use .value here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"listtype" is a number and can't be compared directly with the enum. I could make PlaylistType extend IntEnum and then I could drop the .value in that location. But I can't make the FileType enum extend IntEnum so they'd behave differently and I don't think that's worth the minor convenience in this one spot.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would use IntEnum here and maybe rethink about the filetype enum. Is that filetype enum even required? It looked to me that it makes things just more complicated, but I might be wrong

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good. Actually, in researching how to make enums comparable to ints I came across reasons not to do so (enums aren't ints), but I can instead convert the listtype value to an enum.
if PlaylistType(playlist["listtype"]) == PlaylistType.PODCAST
I might like that better actually.

I like the FileType enum because it more tightly groups the file extensions with the FileType they go with and it adds consistency that the database values that are a limited range of values should be handled as those values.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using IntEnum does not work?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It can work, but while researching how to do so I came across reasons not to do so and I think it's clearer to directly compare the enum rather than mixing enums and ints.

@@ -467,7 +520,7 @@ def set_master(self, tracks):
if self.playlist_voiceover and (Text2Speech.valid_tts['pico2wave'] or Text2Speech.valid_tts['espeak']):
self["dbid"] = hashlib.md5(b"masterlist").digest()[:8]
self.text_to_speech("All songs", self["dbid"], True)
self["listtype"] = 1
self.listtype = PlaylistType.ALL_SONGS
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did you change from struct to class property?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I created the class property self.listtype so I could set it to the enum value and then compare to the enum instead of converting to the .value. I'm not sure why I didn't do the same for the PlaylistHeader. I'd be inclined to make that one match with a new class property, rather than making this match by dropping it.

self.extensions = extensions

# collect all the supported audio extensions
audio_ext = functools.reduce(lambda j,k: j.union(k), map(lambda i: i.extensions, FileType))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesnt it make sense to include that inside the FileType Class?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe I'm missing how to do this, but my attempts haven't succeeded and the examples I have seen for defining this as a static attribute involve extra lines. Also, given that it's right above two other lists of extensions, I don't think it's objectionable to have it outside the class.

@@ -666,7 +725,7 @@ def check_unicode(path):
ret_flag = False # True if there is a recognizable file within this level
for item in os.listdir(path):
if os.path.isfile(os.path.join(path, item)):
if os.path.splitext(item)[1].lower() in audio_ext+list_ext:
if os.path.splitext(item)[1].lower() in all_ext:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think all_ext is really bad to understand. Since it is only used once (i think) we do not need it and can use the better understandable name.

Why does it even check the extension here? The name of the function only checks unicode, so is it even placed correct here (in the first place, was not your initial code).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I figured it made sense to give that union a name and define it with the other lists.

I assume it's doing the extension check to avoid renaming any non-audio files. I'll note that while the code renames non-unicode files to avoid characters that can't be handled by the iPod Shuffle file system, there are some ascii characters that can't be handled as well. Specifically, any file with these characters can't be played: "*/:<>?|\. That might be useful to check for as well, but I figure those make sense in a different PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants