随机随机播放文件,但有一些其他限制


12

我的音乐播放列表很庞大,虽然有些歌手有很多专辑,但有些歌手只有一首歌。我想对播放列表进行排序,以使同一位艺术家不会连续播放两次,或者他的歌曲大部分不会出现在播放列表的开头或结尾。

播放清单范例:

$ 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

sort -R或的输出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.

我期望的是:

$ 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
从技术上讲,您要的是减少随机性,增加结构。这不是不可能,但是将需要一个(bash / awk / perl / python / etc)脚本。
goldilocks 2013年

还是结构化的随机性:)
Teresa e Junior

究竟!这在perl或python中是一个很好的练习。我认为使用bash会让人头疼,尽管使用awk 可能效果很好-我还不太了解awk。
13年

由于似乎没有任何工具可以执行此操作,因此脚本似乎是可行的方法。不是我很懒,而是我没有主意。
Teresa e Junior

1
您可能可以使用简单的算法来做到这一点:通过依次选择每位歌手的随机歌曲来制作播放列表(也可以将歌曲随机分配,但无需重复歌手)。当一位歌手的所有歌曲都用尽后,请以剩余歌手的歌曲(再次,依次交替播放)与现有播放列表的方式进行交织,以最大程度地减少同一位歌手的歌曲邻接。不断重复直到完成。很抱歉,我没有时间将其翻译成实际的脚本。我只是认为对您有所帮助可能会有所帮助。
Joseph R.

Answers:


5

如果我必须将这种改组应用到一副扑克牌上,我想我先将其洗牌,然后在我的眼睛前连续显示纸牌,然后从左到右处理相邻的球杆或心。 。除了一个随机地将所有随机移动到其他位置(尽管不能与另一个相同类型的移动相邻)。

例如,用一只手像

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

基本改组后:

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

两组相邻的黑桃,我们需要重新定位1、2和3。对于1,选择是:

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

我们从那些4中随机选择一个。然后对2和3重复该过程。

实现的perl将是:

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'

如果存在,它将找到与非相邻艺术家的解决方案(除非超过一半的歌曲来自同一位艺术家),并且应统一使用AFAICT。


在尝试了三种不同的脚本(perl和bash)之后,它们全部都洗了我在pastebin上留下的播放列表,而没有留下相邻的歌曲,但是您的似乎以一种更聪明的方式做到了。此外,只有您的人在John B.的例子中表现出色,这无疑是一个最佳答案。我答应德罗伯特接受他的回答,因为他是如此的耐心并对我有所帮助,而且他的第三种方法也非常好。因此,我将为您提供最佳答案和赏金,希望他不要生我的气:)
Teresa e Junior

7

您的示例数据和约束实际上仅允许一些解决方案,例如,您必须每隔一首歌曲播放John B.。我将假设您的实际完整播放列表实质上不是John B,并且随机分配了其他内容

这是另一种随机方法。与@frostschutz的解决方案不同,它可以快速运行。但是,它不能保证结果符合您的标准。我还提出了第二种方法,该方法适用于您的示例数据-但我怀疑会对您的真实数据产生不好的结果。有了您的真实数据(混淆),我添加了方法3-这是一个统一的随机变量,只是它避免了同一位艺术家连续演唱两首歌曲。请注意,它只会对剩余歌曲的“ deck”进行5次“抽奖”,如果之后仍然面对重复的歌手,它将以任何方式输出该歌曲-这样可以保证程序实际上完成。

方法1

基本上,它会在每个点生成一个播放列表,询问“我还有哪些艺术家没有播放过歌曲?” 然后选择一个随机的艺术家,最后从该艺术家中随机选择一首歌曲。(也就是说,每个艺术家的权重均等,而不是与歌曲数量成正比。)

试试看您的实际播放列表,看看它是否比统一播放产生更好的结果。

使用方法:./script-file < input.m3u > output.m3u请确保一定chmod +x要这样做。请注意,它不能正确处理某些M3U文件顶部的签名行...但是您的示例没有。

#!/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};
}

方法2

第二种方法,您可以使用挑选歌曲最多的歌手(而不是我们挑选的最后一位歌手)来代替随机挑选歌手。该程序的最后一段变为:

# 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};
}

该程序的其余部分保持不变。请注意,到目前为止,这并不是最有效的方法,但是对于任何理智大小的播放列表,它应该足够快。使用您的示例数据,所有生成的播放列表将以John B.歌曲开始,然后是Anna A.歌曲,然后是John B.歌曲。之后,它的可预测性要差得多(因为除John B.之外的每个人都只有一首歌)。请注意,这假定使用Perl 5.7或更高版本。

方法3

用法与前面的2相同。请注意0..4,这是5次尝试最大笔数的来源。您可以增加尝试次数,例如,0..9总共可以尝试10次​​。(0..4= 0, 1, 2, 3, 4,您实际上会注意到5个项目)。

#!/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您是否在实际数据上尝试了这两个程序,然后看其中一个是否符合您的喜好?(而且,哇,这很“沉重”。我要添加方法3)
derobert

有些艺术家实际上确实连续播放两次(您可以使用查看sed 's/ - .*//' output.m3u | uniq -d)。您能否解释一下是否照顾到一些没有出现在播放列表开头或结尾的艺术家?
Teresa e Junior

方法1确实确实允许连续两个(或多个)。方法2没有。方法3(即将对其进行编辑)也不会(主要是)。方法2 绝对会加权最常见的艺术家播放列表的开头。方法3不会。
derobert

1
@TeresaeJunior我很高兴第三个人工作了!我不确定4到底是什么方法,但是会很可怕……
derobert

1
@JosephR。方法3 确实使用每个艺术家的歌曲数量作为权重-隐式地通过随机选择一首歌曲。歌手拥有的歌曲越多,就越有可能选择该歌手。#1是唯一不按歌曲数量加权的歌曲。
derobert

2

如果您不介意,效率很低……

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"

它只会不断滚动直到出现连续两个或多个John的结果。如果您的播放列表中有太多约翰,以致于这种组合不存在或极不可能推出,那么它将挂起。

输入的示例结果:

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

如果取消注释调试行,它将告诉您失败的原因:

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

如果它无限期挂起,那应该有助于确定原因。


我喜欢这个主意,但该脚本已运行了将近15m,找不到合适的组合。并不是我的约翰歌太多,但是播放列表超过7000行,这似乎是如何sort设计的。
Teresa e Junior

1
在性能方面,shuf播放列表的随机播放速度是的80倍sort -R。我也不知道!我将使用使其运行15分钟shuf,机会更高!
Teresa e Junior

要进行调试,请echo "$D"在之前if。那应该告诉您哪些重复项阻止了结果的选择。那应该告诉您在哪里寻找问题。(编辑:在答案中添加了可能的调试代码。)
frostschutz

DEBUG总是显示约100行,但来自随机艺术家,因此似乎很多艺术家都在造成此问题。我认为使用sort或确实不可能shuf
Teresa e Junior

1

另一种使用Bash的方法。它以随机顺序读取播放列表,如果该行是重复的,则尝试将其插入到列表的另一端,然后将一个重复对象放在一边以将其重新插入到另一个位置。如果存在三重重复项(第一个,最后一个,并留作相同),它将失败,并且会将那些不良条目附加到列表的末尾。它似乎能够解决您大部分时间上载的大量列表。

#!/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"

可能会更聪明...在您的John示例中,John通常会坚持作为last_artist,因为它总是尝试先附加first_artist。因此,如果它让另外两名艺术家介于两者之间,那么将一位添加到开头,另一位添加到结尾以避开三重John不够聪明。因此,从基本上要求其他每个艺术家都为约翰的列表来看,失败的次数比应有的多。


感谢您的bash脚本。这是我真正真正可以随意理解和修改的唯一方法!
Teresa e Junior
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.