Search code examples
pythonlistsortingplaylist

Changing the order of elements in a list based on current and next value in Python


I am trying to change the order of elements in a list in Python based on the current and next value in that list. I want to use this ordering to create a video playlist that will not contain two consecutive videos of a specific genre. The list will be converted to a m3u playlist.

My situation: I have named all videos that I have in the following way: "movie title" - "year of release" - "genre".

For example, I don't want to have a playlist that will contain two consecutive action movies. The only exception to this should be when I for example use a action movies directory that contains only action movies. Then the playlist can be constructed in a random order.

I currently have the following code:

import os
import glob
from threading import Timer

cwd = os.getcwd()

# create list
videofiles = []

for file in glob.glob('**/*.mp4', recursive=True):
    videofiles.append(file)

# split file on last index to compare genre
def sortSplit(file):
    return file.split('- ',2)[2] 


randomvideo = []
   
for file in videofiles:
          randomvideo.append(sortSplit(file))

randomvideo.sort()

When I use the sortSplit function, I get the index/string that I want to compare. However, I have issues with the following things:

  • Will the split "remember" the original string so that when I construct a list, it will contain the full file name?
  • I can't find a way to compare the current and next elements in the list

For this last part an example list could be:

['movie-year-ACTION', 'movie-year-ACTION', 'movie-year-SCIFI', 'movie-year-DOCUMENTARY']

Where the list ordering should look at ACTION in the first element, compare it to the next element and see that they are the same and switch the next element with a genre that is not ACTION but can be anything other than that. As I expect the amount of genres to grow, I am looking for a way that these genres are not fixed in another list for example. Again, the only exception being that when all elements in the list are of the ACTION genre, just create the list in a random order.

Ofcourse I am open to totally different approaches as long as they serve this purpose.


Solution

  • Algorithm

    forbidden_genre = None
    While there are movies in the database:
      Pick a movie from the non-forbidden genre which has largest remaining number of movies
      Remove that movie from database, add it to playlist
      forbidden_genre = genre of that movie
    

    Note that we always pick a move from one of the genres with largest remaining number of movies, so as to avoid cornering ourselves in a position where one of the genres still has lots of movies and there are not enough movies of other genres to alternate with.

    Python code

    To be able to select movies by genre, we'll start by grouping the movies by genre, using itertools.groupby. In the following code, we can access a genre's name as sortedbysize_groups[genre_index][0] and the list of remaining movies of that genre as sortedbysize_groups[genre_index][1]

    import operator   # itemgetter(2)
    import itertools  # groupby
    
    def make_playlist(videofiles):
      groups_tmp = itertools.groupby(sorted(videofiles, key=operator.itemgetter(2)), operator.itemgetter(2))
      sortedbysize_groups = sorted([(k, list(g)) for k,g in groups_tmp], key=lambda p: len(p[1]))
      playlist = []
      forbidden_genre = None
      while len(sortedbysize_groups) > 1:
        genre_index = get_next_nonforbidden_index(sortedbysize_groups, forbidden_genre)
        next_film = sortedbysize_groups[genre_index][1].pop()
        forbidden_genre = sortedbysize_groups[genre_index][0]
        playlist.append(next_film)
        if len(sortedbysize_groups[genre_index][1]) == 0:
          sortedbysize_groups.pop(genre_index)
        else:
          move_back_if_necessary_to_keep_sorted(sortedbysize_groups, genre_index % len(sortedbysize_groups))
      playlist.extend(sortedbysize_groups[0][1])
      return playlist
    
    def get_next_nonforbidden_index(sortedbysize_groups, forbidden_genre):
      return (-1) if (sortedbysize_groups[-1][0] != forbidden_genre) else (-2)
    
    def move_back_if_necessary_to_keep_sorted(sortedbysize_groups, i):
      while i > 0 and len(sortedbysize_groups[i-1][1]) > len(sortedbysize_groups[-1][1]):
        i -= 1
      if i < len(sortedbysize_groups) - 1:
        sortedbysize_groups[i], sortedbysize_groups[-1] = sortedbysize_groups[-1], sortedbysize_groups[i]
    
    videofiles = [('Star Gate', 1994, 'scifi'), ('Good Will Hunting', 1997, 'drama'), ('A Beautiful Mind', 2001, 'drama'), ('Tenet', 2020, 'scifi'), ('Blade Runner', 1982, 'scifi'), ('The Tree of Life', 2011, 'experimental'), ('Pi', 1998, 'experimental')]
    print(make_playlist(videofiles))
    # [('Blade Runner', 1982, 'scifi'), ('Pi', 1998, 'experimental'), ('A Beautiful Mind', 2001, 'drama'), ('Tenet', 2020, 'scifi'), ('Good Will Hunting', 1997, 'drama'), ('Star Gate', 1994, 'scifi'), ('The Tree of Life', 2011, 'experimental')]
    

    In case where no perfect solution exist because one genre has too many movies, the algorithm will do its best, and the playlist will end with two or more movies of that genre. Note that this only happens if strictly more than half of the movies belong to the same genre.