knicefire 05.06.2011 11:17

PythonОбновление музыки на портативных устройствах (UPDATED)

Привет всем меломанам и остальным welinux'оидам, которые сейчас читают этот пост.

Наверняка не только я один столкнулся с проблемой вызванной необходимостью обновлять музыку у себя на телефоне/плеере.

Под катом я раскажу как я решил эту проблему для себя.

С одной стороны, какая тут проблема? Подключил устройство, удалил немного старых файлов, записал немного новых и все дела.

С другой стороны, если вы слушаете музыку так же как я (всю и в разброс), то зачастую возникает проблема выбора, что удалять что записать и т.д.

В таких ситуациях когда практически одинаково хочешь и того и другого и нельзя выбрать оба сразу (в виду ограниченного размера диска) проще положиться на random.

Хоть я и люблю слушать музыку, но обновлением ее на плеере вручную не всегда охота заниматься, поэтому я написал небольшой скриптик делающий это за меня.

Опции которые принимает скрипт это каталог содержащий всю коллекцию вашей музыки и каталог с музыкой на вашем медиа устройстве.

Так же можно передать параметры:
-f FILLING_PERCENT - процент заполнения диска. Т.е. скрипт никогда не наполнит диск музыкой больше определенного процента. Это полезно если вы ходите иметь некоторый процент доступного пространства для других файлов.
Значение по-умолчанию 80.
-r REFRESHING_PERCENT - процент обновления для старых файлов на устройстве, т.е. сколько музыки (в процентах) будет удалено из устройства перед записью. Значение по-умолчанию 30.
-s, --follow-simlinks - позволяет переходить по ссылкам при поиске музыки. (Отдельное спасибо mrpot за идею)

Т.к. местами скрипт использует *nix специфичные команды, запускаться он будет только в linux среде, но любой желающий может поменять это поведение.

Пример использования:
1
python update_music.py -r 30 -f 50 /home/user/Music /media/My_MP3player/Media/Music


А вот и сам скрипт.
Версия для python 2.6.x-2.7.x update_mysic2.py
  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
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Created with hope to be useful.


import subprocess
import shutil
import os


if os.sys.platform != "linux2":
print "This code for linux platform only!"
exit()


class Song:
import os
def __init__(self, path, collection=None):
self.file_name = os.path.basename(path)
self.collection = collection
self.size = 0
self.full_path = path
self.folder = None
self.type = None
self.size = os.path.getsize(self.full_path)
if self.collection:
path = self.full_path.split(self.collection)[1].lstrip(os.path.sep)
self.folder = os.path.dirname(path)
else:
self.folder = os.path.dirname(self.full_path)
self.type = os.path.splitext(self.file_name)[-1].lstrip('.')

class MusicCollection:
import os
def __init__(self, path, followlinks=False):
self.path = path
self.Songs = []
self.CollectionSize=0
self.allowed_types = ['mp3', 'wma', 'ogg']
self.follow = followlinks
self.scan()

def scan(self):
# we need reinit collection info if we run this outside __init__
self.CollectionSize = 0
self.Songs = []
for root, dirs, files in os.walk(self.path, followlinks=self.follow):
for file in files:
songPath = os.path.join(root, file)
s = Song(songPath, self.path)
if s.type in self.allowed_types:
self.Songs.append(s)
self.CollectionSize += s.size

def add_song(self, song):
self.Songs.append(song)
self.CollectionSize += song.size

def del_song(self, song):
self.Songs.remove(song)
self.CollectionSize -= song.size


def getMount(path):
path = os.path.abspath(path)
while path != os.path.sep:
if os.path.ismount(path):
return path
path = os.path.abspath(os.path.join(path, os.pardir))
return path


def delEmptyDir(path):
for root, dirs, files in os.walk(path):
if not files and not dirs:
# we actually don't need to remove portable music directory
if root != path:
#print 'Deleting empty:', root
os.removedirs(root)
elif not files:
for dir in dirs:
delEmptyDir(os.path.join(root, dir))


def deleteFile(path):
d = os.remove(path)


def copyFile(path, dest):
c = shutil.copy2(path, dest)


def main(*args):
mountPoint = getMount(portableCollection)
if mountPoint == getMount(fullCollection) or mountPoint == '/':
print 'Your portable device not mounted! ' \
'So please mount it and try again.'
exit()
# checking folders to exist
for folder in [fullCollection, portableCollection]:
if not os.path.isdir(folder):
print "Can't find folder %s" % folder
exit()

# disk free check
df = subprocess.Popen(['df'], stdout=subprocess.PIPE).communicate()[0].strip()
for line in df.decode("utf-8").splitlines():
if mountPoint in line:
totalSpace=int(line.split()[1])*1024
freeSpace=int(line.split()[3])*1024
spaceToUse=int(freeSpace - (totalSpace -(totalSpace * fillingPercent/100)))

if refreshingPercent != 0:
# collect files in portableCollection
print "Scanning your portable collection... This may take a while"
pc = MusicCollection(portableCollection, followSimlinks)
filesToDelete=int(len(pc.Songs) * refreshingPercent/100)
# deleting some files to refresh music on portable device
for i in list(range(filesToDelete)):
song = random.choice(pc.Songs)
pc.del_song(song)
space = spaceToUse/1024./1024.
print "Avaliable Space:[%3.1f Mb] Deleting: %s" % (space, song.file_name)
deleteFile(song.full_path)
spaceToUse += song.size
# to clean empty dirs in collection
print "Deleting empty directories"
delEmptyDir(portableCollection)

if spaceToUse < 0:
return
# collect files in fullCollection
print "Scanning your music collection... This may take a while"
fc = MusicCollection(fullCollection, followSimlinks)
# coping random files to disk
while fc.Songs:
song = random.choice(fc.Songs)
fc.Songs.remove(song)
spaceToUse -= song.size
if spaceToUse < 0:
spaceToUse += song.size
break
try:
os.makedirs(os.path.join(portableCollection, song.folder))
except OSError:
pass
#print "Copying:", os.path.join(song.folder, song.file_name)
space = spaceToUse/1024./1024.
print "Avaliable Space:[%3.1f Mb] Copying: %s" % (space, song.file_name)
copyFile(song.full_path, os.path.join(portableCollection, song.folder))


if __name__ == "__main__":
try:
import random
import sys
import optparse

usage = '''run this script with two options:

/directory/with/your/music /directory/with/music/on/your/portable/device

or use -h to get more help'''

parser = optparse.OptionParser(usage=usage)

parser.add_option("-f", "--filling-percent",
help="percent of disk filling", action="store",
default='80')
parser.add_option("-r", "--refreshing-percent",
help="percent of refreshing music", action="store",
default='30')
parser.add_option("-s", "--follow-simlinks",
help="follow simlinks while scaning",
action="store_true", default=False)
(opts, args) = parser.parse_args(sys.argv[1:])

if len(args) != 2:
raise ValueError('Not all directories was specified')

notIntegerMsg = '%s must be integer'
bigMsg = '%s can\'t be more than 100'

if opts.filling_percent:
if not opts.filling_percent.isdigit():
raise Exception(notIntegerMsg % 'Filling percent')
elif int(opts.filling_percent) > 100:
raise Exception(bigMsg % 'Filling percent')
fillingPercent = int(opts.filling_percent)
if opts.refreshing_percent:
if not opts.refreshing_percent.isdigit():
raise Exception(notIntegerMsg % 'Refreshing percent')
elif int(opts.refreshing_percent) > 100:
raise Exception(bigMsg % 'Refreshing percent')
refreshingPercent = int(opts.refreshing_percent)

fullCollection = os.path.abspath(args[0])
portableCollection = os.path.abspath(args[1])
followSimlinks = opts.follow_simlinks

main(fullCollection, portableCollection, fillingPercent,
refreshingPercent, followSimlinks)
print 'Done!'
except KeyboardInterrupt:
print '\nCtl+C detected... Terminated.'
exit()
except (ValueError, Exception) as er:
parser.error(er)
exit()


Версия для python 3.x update_music.py
  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
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Created with hope to be useful.


import subprocess
import shutil
import os


if os.sys.platform != "linux2":
print("This code for linux platform only!")
exit()


class Song:
import os
def __init__(self, path, collection=None):
self.file_name = os.path.basename(path)
self.collection = collection
self.size = 0
self.full_path = path
self.folder = None
self.type = None
self.size = os.path.getsize(self.full_path)
if self.collection:
path = self.full_path.split(self.collection)[1].lstrip(os.path.sep)
self.folder = os.path.dirname(path)
else:
self.folder = os.path.dirname(self.full_path)
self.type = os.path.splitext(self.file_name)[-1].lstrip('.')

class MusicCollection:
import os
def __init__(self, path, followlinks=False):
self.path = path
self.Songs = []
self.CollectionSize=0
self.allowed_types = ['mp3', 'wma', 'ogg']
self.follow = followlinks
self.scan()

def scan(self):
# we need reinit collection info if we run this outside __init__
self.CollectionSize = 0
self.Songs = []
for root, dirs, files in os.walk(self.path, followlinks=self.follow):
for file in files:
songPath = os.path.join(root, file)
s = Song(songPath, self.path)
if s.type in self.allowed_types:
self.Songs.append(s)
self.CollectionSize += s.size

def add_song(self, song):
self.Songs.append(song)
self.CollectionSize += song.size

def del_song(self, song):
self.Songs.remove(song)
self.CollectionSize -= song.size


def getMount(path):
path = os.path.abspath(path)
while path != os.path.sep:
if os.path.ismount(path):
return path
path = os.path.abspath(os.path.join(path, os.pardir))
return path


def delEmptyDir(path):
for root, dirs, files in os.walk(path):
if not files and not dirs:
# we actually don't need to remove portable music directory
if root != path:
#print('Deleting empty:', root)
os.removedirs(root)
elif not files:
for dir in dirs:
delEmptyDir(os.path.join(root, dir))


def deleteFile(path):
d = os.remove(path)


def copyFile(path, dest):
c = shutil.copy2(path, dest)


def main(*args):
mountPoint = getMount(portableCollection)
if mountPoint == getMount(fullCollection) or mountPoint == '/':
print('Your portable device not mounted! '
'So please mount it and try again.')
exit()
# checking folders to exist
for folder in [fullCollection, portableCollection]:
if not os.path.isdir(folder):
print("Can't find folder %s" % folder)
exit()

# disk free check
df = subprocess.Popen(['df'], stdout=subprocess.PIPE).communicate()[0].strip()
for line in df.decode("utf-8").splitlines():
if mountPoint in line:
totalSpace=int(line.split()[1])*1024
freeSpace=int(line.split()[3])*1024
spaceToUse=int(freeSpace - (totalSpace -(totalSpace * fillingPercent/100)))

if refreshingPercent != 0:
# collect files in portableCollection
print("Scanning your portable collection... This may take a while")
pc = MusicCollection(portableCollection, followSimlinks)
filesToDelete=int(len(pc.Songs) * refreshingPercent/100)
# deleting some files to refresh music on portable device
for i in list(range(filesToDelete)):
song = random.choice(pc.Songs)
pc.del_song(song)
space = spaceToUse/1024./1024.
print("Avaliable Space:[%3.1f Mb] Deleting: %s" % (space, song.file_name))
deleteFile(song.full_path)
spaceToUse += song.size
# to clean empty dirs in collection
print("Deleting empty directories")
delEmptyDir(portableCollection)

if spaceToUse < 0:
return
# collect files in fullCollection
print("Scanning your music collection... This may take a while")
fc = MusicCollection(fullCollection, followSimlinks)
# coping random files to disk
while fc.Songs:
song = random.choice(fc.Songs)
fc.Songs.remove(song)
spaceToUse -= song.size
if spaceToUse < 0:
spaceToUse += song.size
break
try:
os.makedirs(os.path.join(portableCollection, song.folder))
except OSError:
pass
#print("Copying:", os.path.join(song.folder, song.file_name))
space = spaceToUse/1024./1024.
print("Avaliable Space:[%3.1f Mb] Copying: %s" % (space, song.file_name))
copyFile(song.full_path, os.path.join(portableCollection, song.folder))


if __name__ == "__main__":
try:
import random
import sys
import optparse

usage = '''run this script with two options:

/directory/with/your/music /directory/with/music/on/your/portable/device

or use -h to get more help'''

parser = optparse.OptionParser(usage=usage)

parser.add_option("-f", "--filling-percent",
help="percent of disk filling", action="store",
default='80')
parser.add_option("-r", "--refreshing-percent",
help="percent of refreshing music", action="store",
default='30')
parser.add_option("-s", "--follow-simlinks",
help="follow simlinks while scaning",
action="store_true", default=False)
(opts, args) = parser.parse_args(sys.argv[1:])

if len(args) != 2:
raise ValueError('Not all directories was specified')

notIntegerMsg = '%s must be integer'
bigMsg = '%s can\'t be more than 100'

if opts.filling_percent:
if not opts.filling_percent.isdigit():
raise Exception(notIntegerMsg % 'Filling percent')
elif int(opts.filling_percent) > 100:
raise Exception(bigMsg % 'Filling percent')
fillingPercent = int(opts.filling_percent)
if opts.refreshing_percent:
if not opts.refreshing_percent.isdigit():
raise Exception(notIntegerMsg % 'Refreshing percent')
elif int(opts.refreshing_percent) > 100:
raise Exception(bigMsg % 'Refreshing percent')
refreshingPercent = int(opts.refreshing_percent)

fullCollection = os.path.abspath(args[0])
portableCollection = os.path.abspath(args[1])
followSimlinks = opts.follow_simlinks

main(fullCollection, portableCollection, fillingPercent,
refreshingPercent, followSimlinks)
print('Done!')
except KeyboardInterrupt:
print('\nCtl+C detected... Terminated.')
exit()
except (ValueError, Exception) as er:
parser.error(er)
exit()



P.S. Надеюсь кому-нибудь будет полезен так же как и мне. :)
P.P.S. Скрипт немного обновлен, добавлены некоторые проверки, переписаны функции копирования и удаления (спасибо K-9)


Тэги: music player python
+ 11 -
Похожие Поделиться

caxap 05.06.2011 18:27 #
что-то я не понял, то есть скрипт зажмуривает глаза за вас? :)
knicefire 05.06.2011 19:25 #
:) можно сказать да.
Он случайным образом удаляет с плеера 30 процентов песен, после чего, опять же, случайным образом выбирает музыку из вашей коллекции и копирует ее на плеер пока диск не заполнится на 80 процентов.
mrpot 07.06.2011 11:26 #
А по симлинкам он переходит, при поиске файлов?
knicefire 07.06.2011 11:37 #
К сожалению, (или к счастью) по симлинкам он не переходит.

Но, если поправить в классе MusicCollection
os.walk(self.path)
на
os.walk(self.path, followlinks=True)
то будет переходить.

Вообще есть в планах доработать скрипт на возможность передавать такого рода параметры в опциях командной строки.
mrpot 07.06.2011 11:46 #
Спасибо.
knicefire 07.06.2011 11:50 #
рад помочь, и, спасибо за идею с переходом по симлинкам. :)
K900 12.06.2011 13:09 #
Что это за издевательство с cp/mv? Чем не устраивают стандартные решения?
knicefire 12.06.2011 23:28 #
да... мне тоже не нравится этот участок... Я обязательно заменю этот код чем-нибудь из shutils.

Может кто-нибудь подскажет универсальный способ определения емкости диска и точки монтирования что бы сделать скрипт кроссплатформенным?
K900 12.06.2011 23:32 #
knicefire 12.06.2011 23:36 #
спасибо.. это оно, уже натыкался но отпугнула сложность... наверное просто нужно внимательнее почититать.
K900 12.06.2011 23:42 #
stat = os.statvfs(mountPoint)
totalSpace = stat.f_bsize * stat.f_blocks
freeSpace = stat.f_bsize * stat.f_bavail
Это в байтах.
knicefire 12.06.2011 23:47 #
отлично! то что нужно. Поставил бы больше плюсов если б можно было..

осталось научиться детектить mountPoint вне *nix.
:)
Можно вопрос? Вам скрипт помог? Ну т.е. вы им пользуетесь? Просто интересно нужно ли это кому-то?
K900 12.06.2011 23:52 #
Мне помог, пользуюсь. Алсо, я таки дятел.
Для венды:
1
2
3
import ctypes
free_bytes = ctypes.c_ulonglong(0)
ctypes.windll.kernel32.GetDiskFreeSpaceExW(ctypes.c_wchar_p(u'c:\\'), None, None, ctypes.pointer(free_bytes))
knicefire 12.06.2011 23:58 #
Это радует.. значит не зря опубликовал.
Наверное пока только заменю supbrocess. :)
K900 13.06.2011 00:02 #
Заменяй cp/mv точно, про df стоит посмотреть. Для венды точка монтирования - это первая буква пути + ":\\".
knicefire 13.06.2011 00:30 #
готово! Так намного лучше. Еще раз спасибо!