Reproduzir aleatoriamente o arquivo aleatoriamente com algumas restrições adicionais


12

Eu tenho uma lista enorme de músicas e, enquanto alguns artistas têm muitos álbuns, outros têm apenas uma música. Eu queria classificar a playlist para que o mesmo artista não tocasse duas vezes seguidas, ou suas músicas não terminariam principalmente no início ou no final da playlist.

Exemplo de lista de reprodução:

$ cat /tmp/playlist.m3u
Anna A. - Song 1
Anna A. - Song 2
I--Rock - Song 1
John B. - Song 1
John B. - Song 2
John B. - Song 3
John B. - Song 4
John B. - Song 5
Kyle C. - Song 1
U--Rock - Song 1

Saída de sort -Rou shuf:

$ sort -R /tmp/playlist.m3u
Anna A. - Song 1 #
U--Rock - Song 1
Anna A. - Song 2 # Anna's songs are all in the beginning.
John B. - Song 2
I--Rock - Song 1
John B. - Song 1
Kyle C. - Song 1
John B. - Song 4 #
John B. - Song 3 #
John B. - Song 5 # Three of John's songs in a row.

O que estou esperando:

$ some_command /tmp/playlist.m3u
John B. - Song 1
Anna A. - Song 1
John B. - Song 2
I--Rock - Song 1
John B. - Song 3
Kyle C. - Song 1
Anna A. - Song 2
John B. - Song 4
U--Rock - Song 1
John B. - Song 5

13
Tecnicamente, o que você está pedindo é menos aleatoriedade e mais estrutura. Não é impossível, mas será necessário um script (bash / awk / perl / python / etc).
Goldilocks

Ou uma aleatoriedade estruturada :)
Teresa e Junior

Exatamente! Este seria um bom exercício em perl ou python. Eu acho que seria uma dor de cabeça com o bash, embora possa funcionar bem com o awk - eu não sei o awk o suficiente para dizer.
goldilocks

Como não parece haver nenhuma ferramenta para fazer isso, um script parece ser o caminho a seguir. Não é que eu seja preguiçoso, mas estou sem idéias.
Teresa e Junior

1
Você pode fazer isso com um algoritmo simples: crie a lista de reprodução selecionando uma música aleatória de cada artista por vez (onde a vez pode ser aleatória também, mas sem repetição do artista). Quando todas as músicas de um artista estiverem esgotadas, comece a intercalar as músicas dos artistas restantes (novamente, alternando entre elas) com a lista de reprodução existente de forma a minimizar a adjacência das músicas do mesmo artista. Continue repetindo até terminar. Sinto muito por não ter tempo para analisar isso em um script real; Eu apenas pensei que poderia ser útil para ajudá-lo a criar o seu próprio.
Joseph R.

Respostas:


5

Se eu tivesse que aplicar esse embaralhamento a um baralho de cartas, acho que embaralharia o baralho primeiro, depois as mostraria seguidas diante dos meus olhos e processaria da esquerda para a direita, onde quer que houvesse clubes ou coração adjacentes. mova todos, exceto um aleatoriamente, para outro lugar (embora não seja próximo a outro do mesmo tipo).

Por exemplo, com uma mão como

🂡 🂢 🂣 🂤 🂥 🂦 🂧 🂨 🂱 🂲 🂳 🃁 🃂 🃃 🃑 🃒

Após o embaralhamento básico:

🂣 🃑 🂲 🂦 🂳 🃁<🂧 🂡 🂨>🃂<🂤 🂢>🃃 🂱 🂥 🃒
                   1  2       3

Para dois grupos de espadas adjacentes, precisamos realocar 1, 2 e 3. Para 1, as opções são:

🂣 🃑 🂲 🂦 🂳 🃁 🂧 🂡 🂨 🃂 🂤 🂢 🃃 🂱 🂥 🃒
    ↑        ↑                    ↑        ↑

Nós escolhemos um aleatoriamente daqueles 4. Em seguida, repetimos o processo para 2 e 3.

Implementado em perlque seria:

shuf list | perl -e '
  @songs = map {/(.*?)-/; [$1,$_]} <>;
  for ($i = 0; $i < @songs; $i++) {
    if (($author = $songs[$i]->[0]) eq $previous) {
      my @reloc_candidates, $same;
      for($j = 0; $j < @songs; $j++) {
        # build a list of positions where we could move that song to
        if ($songs[$j]->[0] eq $author) {$same = 1} else {
          push @reloc_candidates, $j unless $same;
          $same = 0;
        }
      }
      push @reloc_candidates, $j unless $same;

      if (@reloc_candidates) {
        # now pick one of them at random:
        my $chosen = $reloc_candidates[int(rand(@reloc_candidates))];
        splice @songs, $chosen - ($chosen > $i), 0, splice @songs, $i, 1;
        $i -= $chosen > $i;
      }
    }
    $previous = $author;
  }
  print map {$_->[1]} @songs'

Ele encontrará uma solução com artistas não adjacentes, se existir (a menos que mais da metade das músicas sejam do mesmo artista) e deve ser uniforme AFAICT.


Ao tentar os três scripts diferentes (perl e bash), todos embaralham a playlist que eu deixei no pastebin sem deixar músicas adjacentes, mas as suas parecem fazê-lo de uma maneira mais inteligente. Além disso, apenas o seu funciona perfeitamente no exemplo de John B. , o que sem dúvida torna a melhor resposta. Prometi ao derobert aceitar sua resposta, já que ele era muito paciente e prestativo comigo, e sua terceira abordagem também é muito boa. Então, eu vou dar-lhe a melhor resposta e a recompensa para ele, e eu espero que ele não fique com raiva de mim :)
Teresa e Junior

7

Seus dados e restrições de exemplo permitem apenas algumas soluções - você deve tocar John B. todas as outras músicas, por exemplo. Vou assumir que sua lista de reprodução completa não é essencialmente John B, com outras coisas aleatórias para separá-la .

Essa é outra abordagem aleatória. Ao contrário da solução do @ frostschutz, ele roda rapidamente. Entretanto, não garante um resultado que atenda aos seus critérios. Apresento também uma segunda abordagem, que funciona com seus dados de exemplo - mas suspeito que produzirá maus resultados em seus dados reais. Tendo seus dados reais (ofuscados), adiciono a abordagem 3 - que é aleatória uniforme, exceto que evita duas músicas seguidas pelo mesmo artista. Observe que ele só faz 5 "empates" no "deck" das músicas restantes, se depois disso ainda for confrontado com um artista duplicado, ele produzirá essa música de qualquer maneira - dessa forma, é garantido que o programa realmente será concluído.

Abordagem 1

Basicamente, ele gera uma lista de reprodução a cada momento, perguntando "de quais artistas ainda tenho músicas não reproduzidas?" Em seguida, escolha um artista aleatório e, finalmente, uma música aleatória desse artista. (Ou seja, cada artista tem o mesmo peso, não na proporção do número de músicas.)

Experimente a sua lista de reprodução real e veja se ela produz melhores resultados do que uniformemente aleatória.

Uso:./script-file < input.m3u > output.m3u Certifique-se chmod +xdisso, é claro. Observe que ele não lida com a linha de assinatura que está no topo de alguns arquivos M3U corretamente ... mas seu exemplo não tinha isso.

#!/usr/bin/perl
use warnings qw(all);
use strict;

use List::Util qw(shuffle);

# split the input playlist by artist
my %by_artist;
while (defined(my $line = <>)) {
    my $artist = ($line =~ /^(.+?) - /)
        ? $1
        : 'UNKNOWN';
    push @{$by_artist{$artist}}, $line;
}

# sort each artist's songs randomly
foreach my $l (values %by_artist) {
    @$l = shuffle @$l;
}

# pick a random artist, spit out their "last" (remeber: in random order)
# song, remove from the list. If empty, remove artist. Repeat until no
# artists left.
while (%by_artist) {
    my @a_avail = keys %by_artist;
    my $a = $a_avail[int rand @a_avail];
    my $songs = $by_artist{$a};
    print pop @$songs;
    @$songs or delete $by_artist{$a};
}

Abordagem 2

Como segunda abordagem, em vez de escolher um artista aleatório , você pode usar o artista com mais músicas, que também não é o último artista que escolhemos . O parágrafo final do programa passa a ser:

# pick the artist with the most songs who isn't the last artist, spit
# out their "last" (remeber: in random order) song, remove from the
# list. If empty, remove artist. Repeat until no artists left.
my $last_a;
while (%by_artist) {
    my %counts = map { $_, scalar(@{$by_artist{$_}}) } keys %by_artist;
    my @sorted = sort { $counts{$b} <=> $counts{$a} } shuffle keys %by_artist;
    my $a = (1 == @sorted)
        ? $sorted[0]
        : (defined $last_a && $last_a eq $sorted[0])
            ? $sorted[1]
            : $sorted[0];
    $last_a = $a;
    my $songs = $by_artist{$a};
    print pop @$songs;
    @$songs or delete $by_artist{$a};
}

O restante do programa permanece o mesmo. Observe que essa não é a maneira mais eficiente de fazer isso, mas deve ser rápida o suficiente para listas de reprodução de qualquer tamanho. Com seus dados de exemplo, todas as listas de reprodução geradas começarão com uma música de John B., depois uma música de Anna A. e, em seguida, uma música de John B. Depois disso, é muito menos previsível (já que todos, exceto John B., têm uma música). Observe que isso pressupõe Perl 5.7 ou posterior.

Abordagem 3

O uso é o mesmo que o anterior 2. Observe a 0..4parte, é daí que vem o máximo de 5 tentativas. Você poderia aumentar o número de tentativas, por exemplo, 0..9daria 10 no total. ( 0..4= 0, 1, 2, 3, 4, que você notará na verdade são 5 itens).

#!/usr/bin/perl
use warnings qw(all);
use strict;

# read in playlist
my @songs = <>;

# Pick one randomly. Check if its the same artist as the previous song.
# If it is, try another random one. Try again 4 times (5 total). If its
# still the same, accept it anyway.
my $last_artist;
while (@songs) {
    my ($song_idx, $artist);
    for (0..4) {
        $song_idx = int rand @songs;
        $songs[$song_idx] =~ /^(.+?) - /;
        $artist = $1;
        last unless defined $last_artist;
        last unless defined $artist; # assume unknown are all different
        last if $last_artist ne $artist;
    }

    $last_artist = $artist;
    print splice(@songs, $song_idx, 1);
}

@TeresaeJunior você tentou os dois programas com os dados reais e viu se algum deles é do seu agrado? (E, uau, olhando para isso, é muito "FHK Hhck" pesado ... Eu estou indo para adicionar uma abordagem 3)
derobert

Alguns artistas realmente tocam duas vezes seguidas (você pode conferir sed 's/ - .*//' output.m3u | uniq -d). E você poderia explicar se alguns artistas não terminam no início ou no final da lista de reprodução?
Teresa e Junior

A abordagem 1 permite de fato duas (ou mais) seguidas. A abordagem 2 não. A abordagem 3 (prestes a editá-la) também não (bem, principalmente). A abordagem 2 definitivamente avalia o início da lista de reprodução pelos artistas mais comuns. Abordagem 3 não.
derobert

1
@TeresaeJunior Fico feliz que o terceiro tenha funcionado! Não tenho certeza exatamente o que se aproxima 4 teria sido, mas seria assustador ...
derobert

1
@JosephR. Método # 3 não usar o número de músicas de cada artista como um peso-implicitamente, escolhendo uma música aleatória. Quanto mais músicas um artista tiver, maior será a probabilidade de ele ser escolhido. O nº 1 é o único que não pesa pelo número de músicas.
Derobert #

2

Se você não se importa que seja terrivelmente ineficiente ...

while [ 1 ]
do
    R="`shuf playlist`"
    D="`echo "$R" | sed -e 's/ - .*//' | uniq -c -d`"
    if [ "$D" == "" ]
    then
        break
    #else # DEBUG ONLY:
    #    echo --- FAIL: ---
    #    echo "$D"
    #    echo -------------
    fi
done

echo "$R"

Ele continua rolando e rolando até chegar a um resultado que não tem dois ou mais Johns seguidos. Se houver tantos Johns em sua lista de reprodução que essa combinação não exista ou seja improvável que seja lançada, bem, ela travará.

Exemplo de resultado com sua entrada:

John B. - Song 4
Kyle C. - Song 1
Anna A. - Song 2
John B. - Song 3
Anna A. - Song 1
John B. - Song 1
U--Rock - Song 1
John B. - Song 2
I--Rock - Song 1
John B. - Song 5

Se você descomentar as linhas de depuração, ele mostrará o motivo da falha:

--- FAIL: ---
      3 John B.
-------------
--- FAIL: ---
      2 John B.
      2 John B.
-------------

Isso deve ajudar a determinar a causa no caso de travar indefinidamente.


Gosto da ideia, mas o script está em execução há quase 15m e não conseguiu encontrar uma combinação adequada. Não é que eu tenha muitas músicas de John, mas a lista de reprodução tem mais de 7000 linhas, e parece que foi assim que sortfoi criada.
Teresa e Junior

1
Em relação ao desempenho, shufembaralha a lista de reprodução 80 vezes mais rápido que sort -R. Também não sabia disso! Vou deixar por 15 minutos shuf, as chances são maiores!
Teresa e Junior

Para depurar, echo "$D"antes do if. Isso deve indicar quais duplicatas impediram que o resultado fosse escolhido. Isso deve indicar onde procurar o problema. (Edit: Adicionado possível depurar o código para a resposta.)
frostschutz

DEBUG sempre mostra cerca de 100 linhas, mas de artistas aleatórios, então parece que muitos artistas estão causando o problema. Eu acho que não é realmente possível com sortou shuf.
Teresa e Junior

1

Outra abordagem usando o Bash. Ele lê a lista de reprodução em ordem aleatória, tenta inserir a linha no outro extremo da lista, se for uma duplicata, e coloca um único dupe de lado para reinseri-la em outro local. Ele falhará se houver duplicatas triplas (primeira, última e deixadas de lado idênticas) e anexará essas entradas incorretas ao final da lista. Parece ser capaz de resolver a extensa lista que você carregou na maioria das vezes.

#!/bin/bash

first_artist=''
last_artist=''
bad_artist=''
bad_line=''
result=''
bad_result=''

while read line
do
    artist=${line/ - */}
    line="$line"$'\n'

    if [ "$artist" != "$first_artist" ]
    then
        result="$line""$result"
        first_artist="$artist"

        # special case: first = last
        if [ "$last_artist" == '' ]
        then
            last_artist="$artist"
        fi

        # try reinserting bad
        if [ "$bad_artist" != '' -a "$bad_artist" != "$first_artist" ]
        then
            first_artist="$bad_artist"
            result="$bad_line""$result"
            bad_artist=''
            bad_line=''
        fi
    elif [ "$artist" != "$last_artist" ]
    then
        result="$result""$line"
        last_artist="$artist"

        # try reinserting bad
        if [ "$bad_artist" != '' -a "$bad_artist" != "$last_artist" ]
        then
            last_artist="$bad_artist"
            result="$result""$bad_line"
            bad_artist=''
            bad_line=''
        fi
    else
        if [ "$bad_artist" == '' ]
        then
            bad_artist="$artist"
            bad_line="$line"
        else
            # first, last and bad are the same artist :(
            bad_result="$bad_result""$line"
        fi
    fi
done < <(shuf playlist)

# leftovers?
if [ "$bad_artist" != '' ]
then
    bad_result="$bad_result""$bad_line"
fi

echo -n "$result"
echo -n "$bad_result"

Poderia ser mais inteligente ... no seu exemplo de John, John geralmente continuará sendo o last_artist porque sempre tenta anexar o first_artist primeiro. Portanto, se houver dois outros artistas no meio, não é inteligente o suficiente acrescentar um ao começo e o outro ao final para evitar o John triplo. Assim, com listas que basicamente exigem que todos os outros artistas sejam John, você obtém mais falhas do que deveria.


Obrigado por este script bash. É o único que eu realmente consigo entender e modificar à vontade!
Teresa e Junior
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.