summaryrefslogtreecommitdiff
path: root/sortashuffle.py
blob: 1bb7047ea69a50fb82fbb2bb7aed6cb30e3450dd (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
#!/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!

# USAGE
# 0. Copy this script to the playlist folder
# 1. Modify the TARGET variable below
# 2. Modify the SOURCES variable below
# 3. Run the script as administrator
#
# 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()