summaryrefslogtreecommitdiff
path: root/sortashuffle.py
blob: 6a07c4ad93d57a4b76687905415eaf3f7b03042c (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
   #!/usr/bin/env python3
# sortashuffle.py

# REQUIREMENTS
# - Must be run as administrator on Windows
# - All episodes of each show must be in one folder; no season folders!
# - All episodes must be named so that your filesystem sorts them
#    properly in episode order when sorted alphanumerically.
# - All episodes must be named according to my personal conventions.

# NAMING CONVENTIONS
# All files are named suchly:
# show-name.S#.E#.title-of-episode.mp4
#
# The number of seasons and episodes determines how many zeroes are used
# as padding. If there are 99 episodes, episode 1 should be 01. If there
# are 250 episodes, episode 1 should be 001. And so on...
#
# Multi-part episodes have their episode numbers collapsed back to the
# base episode number, and have a, b, c, etc. appended to the episode number.
# This is a destructive operation which loses the true episode number of
# multi-part episodes. For example, if we have a 3-part episode, it
# would look like:
# show-name.S1.E1a.title-of-episode.mp4
# show-name.S1.E1b.title-of-episode.mp4
# show-name.S1.E1c.title-of-episode.mp4
# show-name.S1.E4.title-of-episode.mp4
# Notice the jump from E1c to E4?

# USAGE
# 0. Copy this script to the playlist folder
# 1. Modify the TARGET variable below
# 2. Modify the SOURCES variable below
# 3. Run powershell as administrator
# 4. `cd' into the TARGET directory
# 5. Run the script.
#
# To add/remove shows or regenerate playlist,
# delete everything in TARGET (except this script)
# and re-run the script.

import os
import random


# Must be in format "C://Dir1//Dir2"
TARGET = "<TARGET DIR>"
SOURCES = ["<SOURCE DIR>",
           "<SOURCE DIR>"]


def collect_showlist(sources):
    """Collect show dirs and associated episodes in a nested list.

    Parameters:
        sources (list of str): List of paths to scan for episodes.
            Each path should represent a distinct show, with all
            episodes in one flat directory.

    Returns:
        list of list of str: Nested list structure where:
            - each sub-list represents a show.
            - elements of sub-lists represent episodes.

    Notes:
        - Episode filenames are returned in arbitrary filesystem order.
        - No filtering is performed. All files are included.
        - Does not perform recursive directory scanning
    """
    return [[f for f in os.listdir(source)] for source in sources]


def calculate_weights(showlist):
    """Calculate the weights to be used for selecting a show at random.

    The number of episodes is used as the weight because shows with more
    episodes need to be selected more often in order to maintain an even
    spread of each show across the whole playlist.

    Parameters:
        showlist (list of list of str): Nested list structure where:
            - each sub-list represents a show.
            - elements of sub-lists represent episodes.

    Returns:
        list of int: List where:
            - each element corresponds to a show in `showlist'
            - each element represents the remaining number of episodes.
    """
    return [len(show) for show in showlist]


def select_show(showlist, weightlist):
    """Select a show at random, accounting for weight.

    Parameters:
        showlist (list of list of str): Nested list structure where:
            - each sub-list represents a show.
            - elements of sub-lists represent episodes.
        weightlist (list of int): List where:
            - each element corresponds to a show in `showlist'
            - each element represents the remaining number of episodes.

    Returns:
        int: The list index of a show in the showlist.
    """
    return random.choices(range(len(showlist)),
                          weights=weightlist,
                          k=1)[0]


def shuffle(showlist, weightlist):
    """Shuffle the playlist.

    Parameters:
        showlist (list of list of str): Nested list structure where:
            - each sub-list represents a show.
            - elements of sub-lists represent episodes.
        weightlist (list of int): List where:
            - each element corresponds to a show in `showlist'
            - each element represents the remaining number of episodes.

    Returns:
        shuffled (list of str): Flat list of shuffled episodes.
    """
    shuffled = []
    while any(showlist):
        selection = select_show(showlist, weightlist)
        shuffled.append(SOURCES[selection] + "//" + showlist[selection].pop(0))
        weightlist[selection] = len(showlist[selection])
    return shuffled


def deploy_symlinks(shuffled):
    """Deploy episode symlinks.

    Creates a symlink for each episode in `shuffled'.
    Uses the `count' in the for loop as filename to keep shuffled order.

    Parameters:
        shuffled (list of str): Flat list of shuffled episodes.

    Returns:
        count (int): The number of symlinks deployed.
    """
    count = 0
    for episode in shuffled:
        os.symlink(episode, TARGET + "//" + str(count))
        count += 1
    return count


def deploy_index(shuffled):
    """Deploy playlist index.

    Create a file namned _PLAYLIST_INDEX.txt containing the shuffled
    list of episodes.

    Parameters:
        shuffled (list): Flat list of shuffled episodes.

    Returns:
        index (str): Full path and filename of _PLAYLIST_INDEX.txt.
    """
    index = open(TARGET + "//" + "_PLAYLIST_INDEX.txt", 'w', encoding='utf-8')
    index.write("\n".join(shuffled))
    index.close()
    return index


def main():
    """Shuffle shows but keeps episodes in order."""
    SHOWLIST = collect_showlist(SOURCES)
    WEIGHTLIST = calculate_weights(SHOWLIST)
    SHUFFLED = shuffle(SHOWLIST, WEIGHTLIST)
    deploy_symlinks(SHUFFLED)
    deploy_index(SHUFFLED)
    return 0


if __name__ == "__main__":
    main()