mirror of
https://github.com/OpenVoiceOS/OpenVoiceOS
synced 2025-03-23 15:00:29 +01:00
WIP push for backup (Sorry for the mess)
This commit is contained in:
parent
c6460b9307
commit
a9dc12239f
4
.gitmodules
vendored
4
.gitmodules
vendored
@ -1,4 +1,4 @@
|
||||
[submodule "buildroot"]
|
||||
path = buildroot
|
||||
url = https://github.com/buildroot/buildroot.git
|
||||
branch = 2023.02.x
|
||||
url = https://github.com/j1nx/buildroot.git
|
||||
branch = ovos-2023.02.x
|
||||
|
6
Makefile
6
Makefile
@ -39,6 +39,12 @@ clean:
|
||||
menuconfig:
|
||||
$(MAKE) -C $(BUILDROOT) BR2_EXTERNAL=../$(BUILDROOT_EXTERNAL) menuconfig
|
||||
|
||||
linux-menuconfig:
|
||||
$(MAKE) -C $(BUILDROOT) BR2_EXTERNAL=../$(BUILDROOT_EXTERNAL) linux-menuconfig
|
||||
|
||||
busybox-menuconfig:
|
||||
$(MAKE) -C $(BUILDROOT) BR2_EXTERNAL=../$(BUILDROOT_EXTERNAL) busybox-menuconfig
|
||||
|
||||
savedefconfig:
|
||||
$(MAKE) -C $(BUILDROOT) BR2_EXTERNAL=../$(BUILDROOT_EXTERNAL) savedefconfig
|
||||
|
||||
|
@ -2,5 +2,11 @@ set default="0"
|
||||
set timeout="3"
|
||||
|
||||
menuentry "OpenVoiceOS" {
|
||||
linux /bzImage root=PARTUUID=c0932a41-44cf-463b-8152-d43188553ed4 rootfstype=squashfs ro init=/sbin/pre-init fsck.repair=yes zram.enabled=1 zram.num_devices=4 console=ttyS0 consoleblank=0 loglevel=0 vt.global_cursor_default=0 audit=0 logo.nologo systemd.show_status=0 rootwait quiet
|
||||
linux /bzImage root=PARTUUID=c0932a41-44cf-463b-8152-d43188553ed4 rootfstype=squashfs ro init=/sbin/pre-init fsck.repair=yes zram.enabled=1 zram.num_devices=3 console=tty1 cgroup_enable=cpuset cgroup_memory=1 audit=0 rootwait
|
||||
}
|
||||
menuentry "OpenVoiceOS SystemD Debug" {
|
||||
linux /bzImage root=PARTUUID=c0932a41-44cf-463b-8152-d43188553ed4 rootfstype=squashfs ro init=/sbin/pre-init fsck.repair=yes zram.enabled=1 zram.num_devices=3 console=tty1 cgroup_enable=cpuset cgroup_memory=1 audit=0 rootwait systemd.log_level=debug
|
||||
}
|
||||
menuentry "OpenVoiceOS Recovery" {
|
||||
linux /bzImage root=PARTUUID=c0932a41-44cf-463b-8152-d43188553ed4 rootfstype=squashfs ro init=/sbin/pre-init rootwait systemd.unit=rescue.target
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
243
buildroot-external/configs/ova_64_defconfig
Normal file
243
buildroot-external/configs/ova_64_defconfig
Normal file
@ -0,0 +1,243 @@
|
||||
BR2_x86_64=y
|
||||
BR2_PACKAGE_GLIBC_UTILS=y
|
||||
BR2_BINUTILS_VERSION_2_39_X=y
|
||||
BR2_GCC_VERSION_12_X=y
|
||||
BR2_TOOLCHAIN_BUILDROOT_CXX=y
|
||||
BR2_DL_DIR="../../downloads"
|
||||
BR2_OPTIMIZE_2=y
|
||||
BR2_ENABLE_LTO=y
|
||||
BR2_GLOBAL_PATCH_DIR="../buildroot-patches/"
|
||||
BR2_FORCE_HOST_BUILD=y
|
||||
BR2_SSP_REGULAR=y
|
||||
BR2_TARGET_GENERIC_HOSTNAME="OpenVoiceOS"
|
||||
BR2_TARGET_GENERIC_ISSUE="Welcome to OpenVoiceOS"
|
||||
BR2_TARGET_GENERIC_PASSWD_SHA512=y
|
||||
BR2_INIT_SYSTEMD=y
|
||||
# BR2_TARGET_ENABLE_ROOT_LOGIN is not set
|
||||
BR2_SYSTEM_BIN_SH_BASH=y
|
||||
# BR2_TARGET_GENERIC_GETTY is not set
|
||||
# BR2_TARGET_GENERIC_REMOUNT_ROOTFS_RW is not set
|
||||
BR2_SYSTEM_DHCP="eth0"
|
||||
# BR2_ENABLE_LOCALE_PURGE is not set
|
||||
BR2_GENERATE_LOCALE="en_US.UTF-8"
|
||||
BR2_SYSTEM_ENABLE_NLS=y
|
||||
BR2_ROOTFS_USERS_TABLES="$(BR2_EXTERNAL)/user_table.txt"
|
||||
BR2_ROOTFS_OVERLAY="$(BR2_EXTERNAL)/rootfs-overlay $(BR2_EXTERNAL)/board/ovos/ova/rootfs-overlay"
|
||||
BR2_ROOTFS_POST_BUILD_SCRIPT="$(BR2_EXTERNAL)/board/ovos/ova/post-build.sh"
|
||||
BR2_ROOTFS_POST_IMAGE_SCRIPT="$(BR2_EXTERNAL)/board/ovos/ova/post-image.sh"
|
||||
BR2_ROOTFS_POST_SCRIPT_ARGS="--ova"
|
||||
BR2_LINUX_KERNEL=y
|
||||
BR2_LINUX_KERNEL_CUSTOM_VERSION=y
|
||||
BR2_LINUX_KERNEL_CUSTOM_VERSION_VALUE="6.1.37"
|
||||
BR2_LINUX_KERNEL_DEFCONFIG="x86_64"
|
||||
BR2_LINUX_KERNEL_CONFIG_FRAGMENT_FILES="$(BR2_EXTERNAL)/kernel/ovos.config $(BR2_EXTERNAL)/kernel/device-drivers.config $(BR2_EXTERNAL)/kernel/docker.config $(BR2_EXTERNAL)/board/ovos/ova/kernel.config"
|
||||
BR2_LINUX_KERNEL_LZ4=y
|
||||
BR2_LINUX_KERNEL_NEEDS_HOST_OPENSSL=y
|
||||
BR2_LINUX_KERNEL_NEEDS_HOST_LIBELF=y
|
||||
BR2_PACKAGE_LINUX_TOOLS_HV=y
|
||||
BR2_PACKAGE_LINUX_TOOLS_HV_KVP_DAEMON=y
|
||||
BR2_PACKAGE_LINUX_TOOLS_HV_FCOPY_DAEMON=y
|
||||
BR2_PACKAGE_LINUX_TOOLS_HV_VSS_DAEMON=y
|
||||
BR2_PACKAGE_BUSYBOX_CONFIG="$(BR2_EXTERNAL)/busybox.config"
|
||||
BR2_PACKAGE_ALSA_UTILS=y
|
||||
BR2_PACKAGE_ALSA_UTILS_ALSACONF=y
|
||||
BR2_PACKAGE_ALSA_UTILS_ACONNECT=y
|
||||
BR2_PACKAGE_ALSA_UTILS_ALSALOOP=y
|
||||
BR2_PACKAGE_ALSA_UTILS_ALSAUCM=y
|
||||
BR2_PACKAGE_ALSA_UTILS_ALSATPLG=y
|
||||
BR2_PACKAGE_ALSA_UTILS_AMIDI=y
|
||||
BR2_PACKAGE_ALSA_UTILS_AMIXER=y
|
||||
BR2_PACKAGE_ALSA_UTILS_APLAY=y
|
||||
BR2_PACKAGE_ALSA_UTILS_APLAYMIDI=y
|
||||
BR2_PACKAGE_ALSA_UTILS_ARECORDMIDI=y
|
||||
BR2_PACKAGE_ALSA_UTILS_ASEQDUMP=y
|
||||
BR2_PACKAGE_ALSA_UTILS_ASEQNET=y
|
||||
BR2_PACKAGE_ALSA_UTILS_BAT=y
|
||||
BR2_PACKAGE_ALSA_UTILS_IECSET=y
|
||||
BR2_PACKAGE_ALSA_UTILS_SPEAKER_TEST=y
|
||||
BR2_PACKAGE_PIPEWIRE=y
|
||||
BR2_PACKAGE_BZIP2=y
|
||||
BR2_PACKAGE_GZIP=y
|
||||
BR2_PACKAGE_LZIP=y
|
||||
BR2_PACKAGE_P7ZIP=y
|
||||
BR2_PACKAGE_UNRAR=y
|
||||
BR2_PACKAGE_UNZIP=y
|
||||
BR2_PACKAGE_XZ=y
|
||||
BR2_PACKAGE_ZIP=y
|
||||
BR2_PACKAGE_LSOF=y
|
||||
BR2_PACKAGE_MEMSTAT=y
|
||||
BR2_PACKAGE_NMON=y
|
||||
BR2_PACKAGE_BINUTILS=y
|
||||
BR2_PACKAGE_CHECK=y
|
||||
BR2_PACKAGE_DIFFUTILS=y
|
||||
BR2_PACKAGE_FINDUTILS=y
|
||||
BR2_PACKAGE_GIT_CRYPT=y
|
||||
BR2_PACKAGE_GREP=y
|
||||
BR2_PACKAGE_JO=y
|
||||
BR2_PACKAGE_JQ=y
|
||||
BR2_PACKAGE_LIBTOOL=y
|
||||
BR2_PACKAGE_PATCH=y
|
||||
BR2_PACKAGE_DOSFSTOOLS=y
|
||||
BR2_PACKAGE_DOSFSTOOLS_FATLABEL=y
|
||||
BR2_PACKAGE_DOSFSTOOLS_FSCK_FAT=y
|
||||
BR2_PACKAGE_DOSFSTOOLS_MKFS_FAT=y
|
||||
BR2_PACKAGE_E2FSPROGS=y
|
||||
BR2_PACKAGE_E2FSPROGS_E2IMAGE=y
|
||||
BR2_PACKAGE_E2FSPROGS_FUSE2FS=y
|
||||
BR2_PACKAGE_E2FSPROGS_RESIZE2FS=y
|
||||
BR2_PACKAGE_FUSE_OVERLAYFS=y
|
||||
BR2_PACKAGE_NFS_UTILS=y
|
||||
BR2_PACKAGE_NTFS_3G=y
|
||||
BR2_PACKAGE_SQUASHFS=y
|
||||
# BR2_PACKAGE_SQUASHFS_GZIP is not set
|
||||
BR2_PACKAGE_SQUASHFS_LZ4=y
|
||||
BR2_PACKAGE_LINUX_FIRMWARE=y
|
||||
BR2_PACKAGE_LINUX_FIRMWARE_I915=y
|
||||
BR2_PACKAGE_LINUX_FIRMWARE_IBT=y
|
||||
BR2_PACKAGE_LINUX_FIRMWARE_QUALCOMM_6174A_BT=y
|
||||
BR2_PACKAGE_LINUX_FIRMWARE_RTL_87XX_BT=y
|
||||
BR2_PACKAGE_LINUX_FIRMWARE_RTL_88XX_BT=y
|
||||
BR2_PACKAGE_LINUX_FIRMWARE_IWLWIFI_22000=y
|
||||
BR2_PACKAGE_LINUX_FIRMWARE_IWLWIFI_22260=y
|
||||
BR2_PACKAGE_LINUX_FIRMWARE_IWLWIFI_3160=y
|
||||
BR2_PACKAGE_LINUX_FIRMWARE_IWLWIFI_3168=y
|
||||
BR2_PACKAGE_LINUX_FIRMWARE_IWLWIFI_3945=y
|
||||
BR2_PACKAGE_LINUX_FIRMWARE_IWLWIFI_4965=y
|
||||
BR2_PACKAGE_LINUX_FIRMWARE_IWLWIFI_5000=y
|
||||
BR2_PACKAGE_LINUX_FIRMWARE_IWLWIFI_6000G2A=y
|
||||
BR2_PACKAGE_LINUX_FIRMWARE_IWLWIFI_6000G2B=y
|
||||
BR2_PACKAGE_LINUX_FIRMWARE_IWLWIFI_7260=y
|
||||
BR2_PACKAGE_LINUX_FIRMWARE_IWLWIFI_7265D=y
|
||||
BR2_PACKAGE_LINUX_FIRMWARE_IWLWIFI_8000C=y
|
||||
BR2_PACKAGE_LINUX_FIRMWARE_IWLWIFI_8265=y
|
||||
BR2_PACKAGE_LINUX_FIRMWARE_IWLWIFI_9XXX=y
|
||||
BR2_PACKAGE_LINUX_FIRMWARE_MEDIATEK_MT7601U=y
|
||||
BR2_PACKAGE_LINUX_FIRMWARE_RALINK_RT73=y
|
||||
BR2_PACKAGE_LINUX_FIRMWARE_RALINK_RT2XX=y
|
||||
BR2_PACKAGE_LINUX_FIRMWARE_RTL_81XX=y
|
||||
BR2_PACKAGE_LINUX_FIRMWARE_BNX2X=y
|
||||
BR2_PACKAGE_LINUX_FIRMWARE_RTL_8169=y
|
||||
BR2_PACKAGE_LINUX_FIRMWARE_USB_SERIAL_TI=y
|
||||
BR2_PACKAGE_CRYPTSETUP=y
|
||||
BR2_PACKAGE_GPTFDISK=y
|
||||
BR2_PACKAGE_GPTFDISK_GDISK=y
|
||||
BR2_PACKAGE_GPTFDISK_SGDISK=y
|
||||
BR2_PACKAGE_GPTFDISK_CGDISK=y
|
||||
BR2_PACKAGE_GVFS=y
|
||||
BR2_PACKAGE_PARTED=y
|
||||
BR2_PACKAGE_USB_MODESWITCH_DATA=y
|
||||
BR2_PACKAGE_LUA=y
|
||||
BR2_PACKAGE_ALSA_PLUGINS=y
|
||||
BR2_PACKAGE_LIBSNDFILE=y
|
||||
BR2_PACKAGE_PORTAUDIO=y
|
||||
BR2_PACKAGE_SPEEX=y
|
||||
BR2_PACKAGE_SPEEXDSP=y
|
||||
BR2_PACKAGE_WEBRTC_AUDIO_PROCESSING=y
|
||||
BR2_PACKAGE_LIBARCHIVE=y
|
||||
BR2_PACKAGE_LZO=y
|
||||
BR2_PACKAGE_CA_CERTIFICATES=y
|
||||
BR2_PACKAGE_LIBGPGME=y
|
||||
BR2_PACKAGE_LIBSSH2=y
|
||||
BR2_PACKAGE_LIBOPENSSL_BIN=y
|
||||
BR2_PACKAGE_LIBOPENSSL_ENGINES=y
|
||||
BR2_PACKAGE_LIBLOCKFILE=y
|
||||
BR2_PACKAGE_LIBNFS=y
|
||||
BR2_PACKAGE_LIBSYSFS=y
|
||||
BR2_PACKAGE_LOCKDEV=y
|
||||
BR2_PACKAGE_PHYSFS=y
|
||||
BR2_PACKAGE_WIREPLUMBER=y
|
||||
BR2_PACKAGE_LIBGUDEV=y
|
||||
BR2_PACKAGE_LIBYAML=y
|
||||
BR2_PACKAGE_YAJL=y
|
||||
BR2_PACKAGE_LIBCURL=y
|
||||
BR2_PACKAGE_LIBCURL_CURL=y
|
||||
BR2_PACKAGE_LIBNDP=y
|
||||
BR2_PACKAGE_LIBUNISTRING=y
|
||||
BR2_PACKAGE_PCRE2=y
|
||||
BR2_PACKAGE_BLUEZ_TOOLS=y
|
||||
BR2_PACKAGE_BLUEZ5_UTILS=y
|
||||
BR2_PACKAGE_BLUEZ5_UTILS_OBEX=y
|
||||
BR2_PACKAGE_BLUEZ5_UTILS_CLIENT=y
|
||||
BR2_PACKAGE_BLUEZ5_UTILS_MONITOR=y
|
||||
BR2_PACKAGE_BLUEZ5_UTILS_TOOLS_HID2HCI=y
|
||||
BR2_PACKAGE_CRDA=y
|
||||
BR2_PACKAGE_IPROUTE2=y
|
||||
BR2_PACKAGE_IW=y
|
||||
BR2_PACKAGE_NET_TOOLS=y
|
||||
BR2_PACKAGE_OPENSSH=y
|
||||
# BR2_PACKAGE_OPENSSH_SANDBOX is not set
|
||||
BR2_PACKAGE_WGET=y
|
||||
BR2_PACKAGE_WIRELESS_TOOLS=y
|
||||
BR2_PACKAGE_WPA_SUPPLICANT=y
|
||||
BR2_PACKAGE_WPA_SUPPLICANT_WEXT=y
|
||||
BR2_PACKAGE_WPA_SUPPLICANT_AP_SUPPORT=y
|
||||
BR2_PACKAGE_WPA_SUPPLICANT_MESH_NETWORKING=y
|
||||
BR2_PACKAGE_WPA_SUPPLICANT_AUTOSCAN=y
|
||||
BR2_PACKAGE_WPA_SUPPLICANT_HOTSPOT=y
|
||||
BR2_PACKAGE_WPA_SUPPLICANT_DEBUG_SYSLOG=y
|
||||
BR2_PACKAGE_WPA_SUPPLICANT_WPA3=y
|
||||
BR2_PACKAGE_WPA_SUPPLICANT_CLI=y
|
||||
BR2_PACKAGE_WPA_SUPPLICANT_WPA_CLIENT_SO=y
|
||||
BR2_PACKAGE_WPA_SUPPLICANT_PASSPHRASE=y
|
||||
BR2_PACKAGE_WPA_SUPPLICANT_DBUS=y
|
||||
BR2_PACKAGE_CATATONIT=y
|
||||
BR2_PACKAGE_FILE=y
|
||||
BR2_PACKAGE_SCREEN=y
|
||||
BR2_PACKAGE_SUDO=y
|
||||
BR2_PACKAGE_TIME=y
|
||||
BR2_PACKAGE_TINI=y
|
||||
BR2_PACKAGE_WHICH=y
|
||||
BR2_PACKAGE_ATTR=y
|
||||
BR2_PACKAGE_DOCKER_CLI_BUILDX=y
|
||||
BR2_PACKAGE_DOCKER_COMPOSE=y
|
||||
BR2_PACKAGE_DOCKER_ENGINE=y
|
||||
BR2_PACKAGE_DOCKER_ENGINE_EXPERIMENTAL=y
|
||||
BR2_PACKAGE_EFIBOOTMGR=y
|
||||
BR2_PACKAGE_HTOP=y
|
||||
BR2_PACKAGE_OPENVMTOOLS=y
|
||||
BR2_PACKAGE_PROCPS_NG=y
|
||||
BR2_PACKAGE_SHADOW=y
|
||||
BR2_PACKAGE_SHADOW_SHADOWGRP=y
|
||||
BR2_PACKAGE_SHADOW_ACCOUNT_TOOLS_SETUID=y
|
||||
BR2_PACKAGE_SHADOW_UTMPX=y
|
||||
BR2_PACKAGE_SHADOW_SUBORDINATE_IDS=y
|
||||
# BR2_PACKAGE_SYSTEMD_PSTORE is not set
|
||||
BR2_PACKAGE_SYSTEMD_FIRSTBOOT=y
|
||||
BR2_PACKAGE_SYSTEMD_HIBERNATE=y
|
||||
# BR2_PACKAGE_SYSTEMD_HWDB is not set
|
||||
BR2_PACKAGE_SYSTEMD_LOGIND=y
|
||||
BR2_PACKAGE_SYSTEMD_OOMD=y
|
||||
BR2_PACKAGE_SYSTEMD_RANDOMSEED=y
|
||||
BR2_PACKAGE_SYSTEMD_REPART=y
|
||||
BR2_PACKAGE_SYSTEMD_RFKILL=y
|
||||
# BR2_PACKAGE_SYSTEMD_VCONSOLE is not set
|
||||
BR2_PACKAGE_TAR=y
|
||||
BR2_PACKAGE_UTIL_LINUX_HWCLOCK=y
|
||||
BR2_PACKAGE_UTIL_LINUX_KILL=y
|
||||
BR2_PACKAGE_UTIL_LINUX_LOGGER=y
|
||||
BR2_PACKAGE_UTIL_LINUX_LOGIN=y
|
||||
BR2_PACKAGE_UTIL_LINUX_LSMEM=y
|
||||
BR2_PACKAGE_UTIL_LINUX_MESG=y
|
||||
BR2_PACKAGE_UTIL_LINUX_MORE=y
|
||||
BR2_PACKAGE_UTIL_LINUX_NOLOGIN=y
|
||||
BR2_PACKAGE_UTIL_LINUX_PARTX=y
|
||||
BR2_PACKAGE_UTIL_LINUX_SU=y
|
||||
BR2_PACKAGE_UTIL_LINUX_ZRAMCTL=y
|
||||
BR2_PACKAGE_LESS=y
|
||||
BR2_PACKAGE_NANO=y
|
||||
BR2_PACKAGE_VIM=y
|
||||
BR2_TARGET_ROOTFS_SQUASHFS=y
|
||||
BR2_TARGET_ROOTFS_SQUASHFS4_LZ4=y
|
||||
# BR2_TARGET_ROOTFS_TAR is not set
|
||||
BR2_TARGET_GRUB2=y
|
||||
BR2_TARGET_GRUB2_X86_64_EFI=y
|
||||
BR2_TARGET_GRUB2_BUILTIN_MODULES_EFI="boot linux ext2 fat squash4 part_msdos part_gpt normal efi_gop regexp loadenv echo cat test configfile"
|
||||
BR2_TARGET_GRUB2_INSTALL_TOOLS=y
|
||||
BR2_PACKAGE_HOST_DOSFSTOOLS=y
|
||||
BR2_PACKAGE_HOST_E2FSPROGS=y
|
||||
BR2_PACKAGE_HOST_GENIMAGE=y
|
||||
BR2_PACKAGE_HOST_MKPASSWD=y
|
||||
BR2_PACKAGE_HOST_MTOOLS=y
|
||||
BR2_PACKAGE_HOST_PKGCONF=y
|
||||
BR2_PACKAGE_GROWDISK_SERVICE=y
|
||||
BR2_PACKAGE_HOSTNAME_SERVICE=y
|
85
buildroot-external/kernel/docker.config
Normal file
85
buildroot-external/kernel/docker.config
Normal file
@ -0,0 +1,85 @@
|
||||
CONFIG_POSIX_MQUEUE=y
|
||||
CONFIG_CFQ_GROUP_IOSCHED=y
|
||||
CONFIG_CFS_BANDWIDTH=y
|
||||
CONFIG_FAIR_GROUP_SCHED=y
|
||||
CONFIG_NET_SCHED=y
|
||||
# CONFIG_RT_GROUP_SCHED is not set
|
||||
|
||||
CONFIG_CGROUPS=y
|
||||
CONFIG_HUGETLB_PAGE=y
|
||||
CONFIG_BLK_CGROUP=y
|
||||
CONFIG_BLK_DEV_THROTTLING=y
|
||||
CONFIG_CGROUP_SCHED=y
|
||||
CONFIG_CGROUP_PIDS=y
|
||||
CONFIG_CGROUP_FREEZER=y
|
||||
CONFIG_CGROUP_HUGETLB=y
|
||||
CONFIG_CGROUP_DEVICE=y
|
||||
CONFIG_CGROUP_CPUACCT=y
|
||||
CONFIG_CGROUP_PERF=y
|
||||
CONFIG_CGROUP_HUGETLB=y
|
||||
CONFIG_NET_CLS_CGROUP=y
|
||||
CONFIG_CGROUP_NET_PRIO=y
|
||||
CONFIG_CGROUP_BPF=y
|
||||
CONFIG_BPF_SYSCALL=y
|
||||
|
||||
CONFIG_MEMCG=y
|
||||
CONFIG_MEMCG_SWAP=y
|
||||
|
||||
CONFIG_NAMESPACES=y
|
||||
CONFIG_USER_NS=y
|
||||
CONFIG_PID_NS=y
|
||||
CONFIG_IPC_NS=y
|
||||
CONFIG_UTS_NS=y
|
||||
|
||||
CONFIG_NETDEVICES=y
|
||||
CONFIG_DUMMY=m
|
||||
CONFIG_MACVLAN=m
|
||||
CONFIG_IPVLAN=m
|
||||
CONFIG_VXLAN=m
|
||||
|
||||
CONFIG_INET=y
|
||||
CONFIG_IPV6=y
|
||||
CONFIG_INET_ESP=m
|
||||
CONFIG_INET_XFRM_MODE_TRANSPORT=m
|
||||
CONFIG_NETCONSOLE=y
|
||||
CONFIG_VETH=y
|
||||
CONFIG_NETFILTER=y
|
||||
CONFIG_NF_CONNTRACK=y
|
||||
CONFIG_NF_NAT=y
|
||||
CONFIG_NF_NAT_NEEDED=y
|
||||
CONFIG_NF_CONNTRACK_IPV4=y
|
||||
CONFIG_IP6_NF_IPTABLES=y
|
||||
CONFIG_IP6_NF_FILTER=y
|
||||
CONFIG_IP6_NF_MANGLE=y
|
||||
CONFIG_IP6_NF_NAT=y
|
||||
CONFIG_NETFILTER_ADVANCED=y
|
||||
CONFIG_NETFILTER_XT_MATCH_ADDRTYPE=y
|
||||
CONFIG_NETFILTER_XT_MATCH_CONNTRACK=y
|
||||
CONFIG_NETFILTER_XT_MATCH_IPVS=y
|
||||
CONFIG_IP_VS=y
|
||||
CONFIG_IP_VS_RR=y
|
||||
CONFIG_IP_VS_NFCT=y
|
||||
CONFIG_IP_NF_IPTABLES=y
|
||||
CONFIG_IP_NF_FILTER=y
|
||||
CONFIG_IP_NF_NAT=y
|
||||
CONFIG_IP_NF_TARGET_MASQUERADE=y
|
||||
CONFIG_IP_NF_TARGET_REDIRECT=y
|
||||
CONFIG_BRIDGE=y
|
||||
CONFIG_BRIDGE_NETFILTER=y
|
||||
CONFIG_XFRM=m
|
||||
CONFIG_XFRM_USER=m
|
||||
CONFIG_XFRM_ALGO=m
|
||||
CONFIG_NET_L3_MASTER_DEV=y
|
||||
|
||||
CONFIG_EXT4_FS=y
|
||||
CONFIG_EXT4_FS_POSIX_ACL=y
|
||||
CONFIG_EXT4_FS_SECURITY=y
|
||||
CONFIG_OVERLAY_FS=y
|
||||
CONFIG_OVERLAY_FS_METACOPY=y
|
||||
|
||||
CONFIG_CRYPTO_CCM=m
|
||||
CONFIG_CRYPTO_GCM=m
|
||||
CONFIG_CRYPTO_CMAC=m
|
||||
CONFIG_CRYPTO_ARC4=m
|
||||
|
||||
CONFIG_DEVPTS_MULTIPLE_INSTANCES=y
|
@ -30,8 +30,10 @@ CONFIG_SQUASHFS_LZ4=y
|
||||
CONFIG_BTRFS_FS=m
|
||||
CONFIG_OVERLAY_FS=y
|
||||
|
||||
CONFIG_SECCOMP=y
|
||||
CONFIG_SECCOMP_FILTER=y
|
||||
# CONFIG_SECCOMP is not set
|
||||
# CONFIG_AUDIT is not set
|
||||
# CONFIG_SECURITY is not set
|
||||
# CONFIG_SECURITY_SELINUX is not set
|
||||
|
||||
CONFIG_CRYPTO=y
|
||||
CONFIG_CRYPTO_LZ4=y
|
||||
|
@ -1,22 +0,0 @@
|
||||
[main]
|
||||
dns=default
|
||||
plugins=keyfile
|
||||
autoconnect-retries-default=0
|
||||
rc-manager=file
|
||||
|
||||
[keyfile]
|
||||
unmanaged-devices=type:tun;type:veth
|
||||
|
||||
[logging]
|
||||
backend=journal
|
||||
|
||||
[connection]
|
||||
connection.mdns=2
|
||||
connection.llmnr=2
|
||||
|
||||
[connectivity]
|
||||
uri=http://nmcheck.gnome.org/check_network_status.txt
|
||||
interval=300
|
||||
|
||||
[device]
|
||||
wifi.scan-rand-mac-address=no
|
@ -1,2 +0,0 @@
|
||||
[main]
|
||||
auth-polkit=true
|
@ -1,3 +0,0 @@
|
||||
[connection]
|
||||
# Values are 0 (use default), 1 (ignore/don't touch), 2 (disable) or 3 (enable).
|
||||
wifi.powersave = 2
|
@ -1,11 +0,0 @@
|
||||
[connection]
|
||||
id=OpenVoiceOS default
|
||||
uuid=554628d6-8290-3dea-90c1-9b3b108dc19c
|
||||
type=802-3-ethernet
|
||||
|
||||
[ipv4]
|
||||
method=auto
|
||||
|
||||
[ipv6]
|
||||
addr-gen-mode=stable-privacy
|
||||
method=auto
|
@ -1,14 +0,0 @@
|
||||
# Use PulseAudio by default
|
||||
pcm.!default {
|
||||
type pulse
|
||||
fallback "sysdefault"
|
||||
hint {
|
||||
show on
|
||||
description "Default ALSA Output (currently PulseAudio Sound Server)"
|
||||
}
|
||||
}
|
||||
|
||||
ctl.!default {
|
||||
type pulse
|
||||
fallback "sysdefault"
|
||||
}
|
@ -1,78 +0,0 @@
|
||||
{
|
||||
"ready_settings": ["setup", "skills"],
|
||||
"confirm_listening": true,
|
||||
"play_wav_cmdline": "paplay %1",
|
||||
"play_mp3_cmdline": "mpg123 %1",
|
||||
"ipc_path": "/dev/shm/mycroft/ipc/",
|
||||
"network_tests": {
|
||||
"ip_url": "https://api.ipify.org",
|
||||
"dns_primary": "1.1.1.1",
|
||||
"dns_secondary": "8.8.8.8",
|
||||
"web_url": "http://nmcheck.gnome.org/check_network_status.txt",
|
||||
"web_url_secondary": "https://checkonline.home-assistant.io/online.txt",
|
||||
"captive_portal_url": "http://nmcheck.gnome.org/check_network_status.txt",
|
||||
"captive_portal_text": "NetworkManager is online"
|
||||
},
|
||||
"gui": {
|
||||
"extension": "smartspeaker",
|
||||
"idle_display_skill": "skill-ovos-homescreen.openvoiceos"
|
||||
},
|
||||
"PHAL": {
|
||||
"ovos-PHAL-plugin-system": {"sudo": false}
|
||||
},
|
||||
"listener": {
|
||||
"mute_during_output": false,
|
||||
"instant_listen": true,
|
||||
"VAD": {
|
||||
"module": "ovos-vad-plugin-webrtcvad",
|
||||
"ovos-vad-plugin-webrtcvad": {"vad_mode": 3}
|
||||
},
|
||||
"retry_mic_init" : false
|
||||
},
|
||||
"hotwords": {
|
||||
"hey_mycroft": {
|
||||
"module": "ovos-precise-lite",
|
||||
"model": "~/.local/share/precise-lite/wakewords/en/hey_mycroft.tflite",
|
||||
"sensitivity": 0.5,
|
||||
"trigger_level": 3,
|
||||
"expected_duration": 3
|
||||
}
|
||||
},
|
||||
"stt": {
|
||||
"fallback_module": ""
|
||||
},
|
||||
"tts": {
|
||||
"module": "ovos-tts-plugin-mimic3-server",
|
||||
"fallback_module": "ovos-tts-plugin-mimic",
|
||||
"ovos-tts-plugin-mimic3-server": {
|
||||
"voice": "en_UK/apope_low"
|
||||
}
|
||||
},
|
||||
"skills": {
|
||||
"wait_for_internet": true,
|
||||
"autogen_meta": false
|
||||
},
|
||||
"Audio": {
|
||||
"backends": {
|
||||
"OCP": {
|
||||
"type": "ovos_common_play",
|
||||
"manage_external_players": true,
|
||||
"active": true,
|
||||
"youtube_backend": "youtube-dl",
|
||||
"ydl_backend": "auto"
|
||||
},
|
||||
"vlc": {
|
||||
"type": "ovos_vlc",
|
||||
"active": true
|
||||
},
|
||||
"simple": {
|
||||
"type": "ovos_audio_simple",
|
||||
"active": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"log_level": "INFO",
|
||||
"logs": {
|
||||
"path": "/var/log/mycroft"
|
||||
}
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
/* give group 'network' rights to change settings */
|
||||
/* taken from https://wiki.archlinux.org/index.php/NetworkManager#Set_up_PolicyKit_permissions */
|
||||
|
||||
polkit.addRule(function(action, subject) {
|
||||
if (action.id.indexOf("org.freedesktop.NetworkManager.") == 0 && subject.isInGroup("network")) {
|
||||
return polkit.Result.YES;
|
||||
}
|
||||
});
|
@ -1,45 +0,0 @@
|
||||
[global]
|
||||
workgroup = OPENVOICEOS
|
||||
netbios name = OVOS
|
||||
log file = /var/log/samba/log.%m
|
||||
max log size = 1000
|
||||
logging = file
|
||||
panic action = /usr/share/samba/panic-action %d
|
||||
server role = standalone server
|
||||
obey pam restrictions = yes
|
||||
unix password sync = yes
|
||||
passwd program = /usr/bin/passwd %u
|
||||
passwd chat = *Enter\snew\s*\spassword:* %n\n *Retype\snew\s*\spassword:* %n\n *password\supdated\ssuccessfully* .
|
||||
pam password change = yes
|
||||
map to guest = bad user
|
||||
usershare allow guests = yes
|
||||
|
||||
[Pictures]
|
||||
path = /home/mycroft/Pictures
|
||||
public = yes
|
||||
guest only = yes
|
||||
writable = yes
|
||||
force create mode = 0666
|
||||
force directory mode = 0777
|
||||
browseable = yes
|
||||
force user = mycroft
|
||||
|
||||
[Documents]
|
||||
path = /home/mycroft/Documents
|
||||
public = yes
|
||||
guest only = yes
|
||||
writable = yes
|
||||
force create mode = 0666
|
||||
force directory mode = 0777
|
||||
browseable = yes
|
||||
force user = mycroft
|
||||
|
||||
[Music]
|
||||
path = /home/mycroft/Music
|
||||
public = yes
|
||||
guest only = yes
|
||||
writable = yes
|
||||
force create mode = 0666
|
||||
force directory mode = 0777
|
||||
browseable = yes
|
||||
force user = mycroft
|
@ -1,6 +0,0 @@
|
||||
general = {
|
||||
name = "OpenVoiceOS";
|
||||
output_backend = "pa";
|
||||
dbus_service_bus = "session";
|
||||
mpris_service_bus = "session";
|
||||
};
|
@ -1 +0,0 @@
|
||||
../../../usr/lib/systemd/system/ovos.target
|
@ -1,3 +0,0 @@
|
||||
d /run/pulse 755 pulse pulse
|
||||
d /run/pulse/.config 755 pulse pulse
|
||||
d /run/pulse/.config/pulse 755 pulse pulse
|
@ -1,53 +0,0 @@
|
||||
# Configuration file for the usbmount package, which mounts removable
|
||||
# storage devices when they are plugged in and unmounts them when they
|
||||
# are removed.
|
||||
|
||||
# Change to zero to disable usbmount
|
||||
ENABLED=1
|
||||
|
||||
# Mountpoints: These directories are eligible as mointpoints for
|
||||
# removable storage devices. A newly plugged in device is mounted on
|
||||
# the first directory in this list that exists and on which nothing is
|
||||
# mounted yet.
|
||||
MOUNTPOINTS="/media/usb0 /media/usb1 /media/usb2 /media/usb3
|
||||
/media/usb4 /media/usb5 /media/usb6 /media/usb7"
|
||||
|
||||
# Filesystem types: removable storage devices are only mounted if they
|
||||
# contain a filesystem type which is in this list.
|
||||
FILESYSTEMS="vfat ntfs ext2 ext3 ext4 hfsplus exfat f2fs"
|
||||
|
||||
#############################################################################
|
||||
# WARNING! #
|
||||
# #
|
||||
# The "sync" option may not be a good choice to use with flash drives, as #
|
||||
# it forces a greater amount of writing operating on the drive. This makes #
|
||||
# the writing speed considerably lower and also leads to a faster wear out #
|
||||
# of the disk. #
|
||||
# #
|
||||
# If you omit it, don't forget to use the command "sync" to synchronize the #
|
||||
# data on your disk before removing the drive or you may experience data #
|
||||
# loss. #
|
||||
# #
|
||||
# It is highly recommended that you use the pumount command (as a regular #
|
||||
# user) before unplugging the device. It makes calling the "sync" command #
|
||||
# and mounting with the sync option unnecessary---this is similar to other #
|
||||
# operating system's "safely disconnect the device" option. #
|
||||
#############################################################################
|
||||
# Mount options: Options passed to the mount command with the -o flag.
|
||||
# See the warning above regarding removing "sync" from the options.
|
||||
MOUNTOPTIONS="noexec,nodev,noatime,nodiratime"
|
||||
|
||||
# Filesystem type specific mount options: This variable contains a space
|
||||
# separated list of strings, each which the form "-fstype=TYPE,OPTIONS".
|
||||
#
|
||||
# If a filesystem with a type listed here is mounted, the corresponding
|
||||
# options are appended to those specificed in the MOUNTOPTIONS variable.
|
||||
#
|
||||
# For example, "-fstype=vfat,gid=floppy,dmask=0007,fmask=0117" would add
|
||||
# the options "gid=floppy,dmask=0007,fmask=0117" when a vfat filesystem
|
||||
# is mounted.
|
||||
FS_MOUNTOPTIONS="fstype=vfat,utf8,uid=1000,gid=1000,umask=022 -fstype=ntfs-3g,nls=utf8,uid=1000,gid=1000,umask=022"
|
||||
|
||||
# If set to "yes", more information will be logged via the syslog
|
||||
# facility.
|
||||
VERBOSE=no
|
@ -1,12 +0,0 @@
|
||||
{
|
||||
"stt": { "module": "google"},
|
||||
"backend_port": 6712,
|
||||
"geolocate": true,
|
||||
"override_location": false,
|
||||
"api_version": "v1",
|
||||
"data_path": "~",
|
||||
"record_utterances": false,
|
||||
"record_wakewords": false,
|
||||
"wolfram_key": "Y7R353-9HQAAL8KKA",
|
||||
"owm_key": "28fed22898afd4717ce5a1535da1f78c"
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
general = {
|
||||
name = "OpenVoiceOS";
|
||||
output_backend = "pa";
|
||||
dbus_service_bus = "session";
|
||||
mpris_service_bus = "session";
|
||||
};
|
@ -1,90 +0,0 @@
|
||||
[spotifyd]
|
||||
# Your Spotify account name.
|
||||
#username = "username"
|
||||
|
||||
# Your Spotify account password.
|
||||
#password = "password"
|
||||
|
||||
# A command that gets executed and can be used to
|
||||
# retrieve your password.
|
||||
# The command should return the password on stdout.
|
||||
#
|
||||
# This is an alternative to the `password` field. Both
|
||||
# can't be used simultaneously.
|
||||
#password_cmd = "command_that_writes_password_to_stdout"
|
||||
|
||||
# If set to true, `spotifyd` tries to look up your
|
||||
# password in the system's password storage.
|
||||
#
|
||||
# This is an alternative to the `password` field. Both
|
||||
# can't be used simultaneously.
|
||||
#use_keyring = true
|
||||
|
||||
#
|
||||
# If set to true, `spotifyd` tries to bind to the session dbus
|
||||
# and expose MPRIS controls. When running headless, without a dbus session,
|
||||
# then set this to false to avoid binding errors
|
||||
#
|
||||
use_mpris = true
|
||||
|
||||
# The audio backend used to play the your music. To get
|
||||
# a list of possible backends, run `spotifyd --help`.
|
||||
backend = "pulseaudio"
|
||||
|
||||
# The alsa audio device to stream audio to. To get a
|
||||
# list of valid devices, run `aplay -L`,
|
||||
#device = "alsa_audio_device" # omit for macOS
|
||||
|
||||
# The alsa control device. By default this is the same
|
||||
# name as the `device` field.
|
||||
#control = "alsa_audio_device" # omit for macOS
|
||||
|
||||
# The alsa mixer used by `spotifyd`.
|
||||
#mixer = "PCM"
|
||||
|
||||
# The volume controller. Each one behaves different to
|
||||
# volume increases. For possible values, run
|
||||
# `spotifyd --help`.
|
||||
#volume_controller = "alsa" # use softvol for macOS
|
||||
|
||||
# A command that gets executed in your shell after each song changes.
|
||||
#on_song_change_hook = "command_to_run_on_playback_events"
|
||||
|
||||
# The name that gets displayed under the connect tab on
|
||||
# official clients. Spaces are not allowed!
|
||||
device_name = "OpenVoiceOS"
|
||||
|
||||
# The audio bitrate. 96, 160 or 320 kbit/s
|
||||
#bitrate = 160
|
||||
|
||||
# The directory used to cache audio data. This setting can save
|
||||
# a lot of bandwidth when activated, as it will avoid re-downloading
|
||||
# audio files when replaying them.
|
||||
#
|
||||
# Note: The file path does not get expanded. Environment variables and
|
||||
# shell placeholders like $HOME or ~ don't work!
|
||||
#cache_path = "cache_directory"
|
||||
|
||||
# If set to true, audio data does NOT get cached.
|
||||
#no_audio_cache = true
|
||||
|
||||
# Volume on startup between 0 and 100
|
||||
# NOTE: This variable's type will change in v0.4, to a number (instead of string)
|
||||
#initial_volume = "90"
|
||||
|
||||
# If set to true, enables volume normalisation between songs.
|
||||
#volume_normalisation = true
|
||||
|
||||
# The normalisation pregain that is applied for each song.
|
||||
#normalisation_pregain = -10
|
||||
|
||||
# The port `spotifyd` uses to announce its service over the network.
|
||||
zeroconf_port = 57621
|
||||
|
||||
# The proxy `spotifyd` will use to connect to spotify.
|
||||
#proxy = "http://proxy.example.org:8080"
|
||||
|
||||
# The displayed device type in Spotify clients.
|
||||
# Can be unknown, computer, tablet, smartphone, speaker, t_v,
|
||||
# a_v_r (Audio/Video Receiver), s_t_b (Set-Top Box), and audio_dongle.
|
||||
device_type = "speaker"
|
@ -1 +0,0 @@
|
||||
../../../../../../usr/lib/systemd/user/kdeconnectd.service
|
@ -1 +0,0 @@
|
||||
../../../../../../usr/lib/systemd/user/mycroft.service
|
@ -1 +0,0 @@
|
||||
../../../../../../usr/lib/systemd/user/pulseaudio.service
|
@ -1 +0,0 @@
|
||||
../../../../../../usr/lib/systemd/user/shairport-sync.service
|
@ -1 +0,0 @@
|
||||
../../../../../../usr/lib/systemd/user/spotifyd.service
|
@ -1 +0,0 @@
|
||||
../../../../../../usr/lib/systemd/user/mycroft-audio.service
|
@ -1 +0,0 @@
|
||||
../../../../../../usr/lib/systemd/user/mycroft-messagebus.service
|
@ -1 +0,0 @@
|
||||
../../../../../../usr/lib/systemd/user/mycroft-phal.service
|
@ -1 +0,0 @@
|
||||
../../../../../../usr/lib/systemd/user/mycroft-skills.service
|
@ -1 +0,0 @@
|
||||
../../../../../../usr/lib/systemd/user/mycroft-voice.service
|
@ -1 +0,0 @@
|
||||
../../../../../../usr/lib/systemd/user/pulseaudio.socket
|
@ -1,606 +0,0 @@
|
||||
======
|
||||
Colour
|
||||
======
|
||||
|
||||
.. image:: http://img.shields.io/pypi/v/colour.svg?style=flat
|
||||
:target: https://pypi.python.org/pypi/colour/
|
||||
:alt: Latest PyPI version
|
||||
|
||||
.. image:: https://img.shields.io/pypi/l/gitchangelog.svg?style=flat
|
||||
:target: https://github.com/vaab/gitchangelog/blob/master/LICENSE
|
||||
:alt: License
|
||||
|
||||
.. image:: https://img.shields.io/pypi/pyversions/gitchangelog.svg?style=flat
|
||||
:target: https://pypi.python.org/pypi/gitchangelog/
|
||||
:alt: Compatible python versions
|
||||
|
||||
.. image:: http://img.shields.io/pypi/dm/colour.svg?style=flat
|
||||
:target: https://pypi.python.org/pypi/colour/
|
||||
:alt: Number of PyPI downloads
|
||||
|
||||
.. image:: http://img.shields.io/travis/vaab/colour/master.svg?style=flat
|
||||
:target: https://travis-ci.org/vaab/colour/
|
||||
:alt: Travis CI build status
|
||||
|
||||
.. image:: https://img.shields.io/appveyor/ci/vaab/colour.svg
|
||||
:target: https://ci.appveyor.com/project/vaab/colour/branch/master
|
||||
:alt: Appveyor CI build status
|
||||
|
||||
.. image:: http://img.shields.io/codecov/c/github/vaab/colour.svg?style=flat
|
||||
:target: https://codecov.io/gh/vaab/colour/
|
||||
:alt: Test coverage
|
||||
|
||||
|
||||
Converts and manipulates common color representation (RGB, HSL, web, ...)
|
||||
|
||||
|
||||
Feature
|
||||
=======
|
||||
|
||||
- Damn simple and pythonic way to manipulate color representation (see
|
||||
examples below)
|
||||
|
||||
- Full conversion between RGB, HSL, 6-digit hex, 3-digit hex, human color
|
||||
|
||||
- One object (``Color``) or bunch of single purpose function (``rgb2hex``,
|
||||
``hsl2rgb`` ...)
|
||||
|
||||
- ``web`` format that use the smallest representation between
|
||||
6-digit (e.g. ``#fa3b2c``), 3-digit (e.g. ``#fbb``), fully spelled
|
||||
color (e.g. ``white``), following `W3C color naming`_ for compatible
|
||||
CSS or HTML color specifications.
|
||||
|
||||
- smooth intuitive color scale generation choosing N color gradients.
|
||||
|
||||
- can pick colors for you to identify objects of your application.
|
||||
|
||||
|
||||
.. _W3C color naming: http://www.w3.org/TR/css3-color/#svg-color
|
||||
|
||||
|
||||
Installation
|
||||
============
|
||||
|
||||
You don't need to download the GIT version of the code as ``colour`` is
|
||||
available on the PyPI. So you should be able to run::
|
||||
|
||||
pip install colour
|
||||
|
||||
If you have downloaded the GIT sources, then you could add the ``colour.py``
|
||||
directly to one of your ``site-packages`` (thanks to a symlink). Or install
|
||||
the current version via traditional::
|
||||
|
||||
python setup.py install
|
||||
|
||||
And if you don't have the GIT sources but would like to get the latest
|
||||
master or branch from github, you could also::
|
||||
|
||||
pip install git+https://github.com/vaab/colour
|
||||
|
||||
Or even select a specific revision (branch/tag/commit)::
|
||||
|
||||
pip install git+https://github.com/vaab/colour@master
|
||||
|
||||
|
||||
Usage
|
||||
=====
|
||||
|
||||
To get complete demo of each function, please read the source code which is
|
||||
heavily documented and provide a lot of examples in doctest format.
|
||||
|
||||
Here is a reduced sample of a common usage scenario:
|
||||
|
||||
|
||||
Instantiation
|
||||
-------------
|
||||
|
||||
Let's create blue color::
|
||||
|
||||
>>> from colour import Color
|
||||
>>> c = Color("blue")
|
||||
>>> c
|
||||
<Color blue>
|
||||
|
||||
Please note that all of these are equivalent examples to create the red color::
|
||||
|
||||
Color("red") ## human, web compatible representation
|
||||
Color(red=1) ## default amount of blue and green is 0.0
|
||||
Color("blue", hue=0) ## hue of blue is 0.66, hue of red is 0.0
|
||||
Color("#f00") ## standard 3 hex digit web compatible representation
|
||||
Color("#ff0000") ## standard 6 hex digit web compatible representation
|
||||
Color(hue=0, saturation=1, luminance=0.5)
|
||||
Color(hsl=(0, 1, 0.5)) ## full 3-uple HSL specification
|
||||
Color(rgb=(1, 0, 0)) ## full 3-uple RGB specification
|
||||
Color(Color("red")) ## recursion doesn't break object
|
||||
|
||||
|
||||
Reading values
|
||||
--------------
|
||||
|
||||
Several representations are accessible::
|
||||
|
||||
>>> c.hex
|
||||
'#00f'
|
||||
>>> c.hsl # doctest: +ELLIPSIS
|
||||
(0.66..., 1.0, 0.5)
|
||||
>>> c.rgb
|
||||
(0.0, 0.0, 1.0)
|
||||
|
||||
And their different parts are also independently accessible, as the different
|
||||
amount of red, blue, green, in the RGB format::
|
||||
|
||||
>>> c.red
|
||||
0.0
|
||||
>>> c.blue
|
||||
1.0
|
||||
>>> c.green
|
||||
0.0
|
||||
|
||||
Or the hue, saturation and luminance of the HSL representation::
|
||||
|
||||
>>> c.hue # doctest: +ELLIPSIS
|
||||
0.66...
|
||||
>>> c.saturation
|
||||
1.0
|
||||
>>> c.luminance
|
||||
0.5
|
||||
|
||||
A note on the ``.hex`` property, it'll return the smallest valid value
|
||||
when possible. If you are only interested by the long value, use
|
||||
``.hex_l``::
|
||||
|
||||
>>> c.hex_l
|
||||
'#0000ff'
|
||||
|
||||
|
||||
Modifying color objects
|
||||
-----------------------
|
||||
|
||||
All of these properties are read/write, so let's add some red to this color::
|
||||
|
||||
>>> c.red = 1
|
||||
>>> c
|
||||
<Color magenta>
|
||||
|
||||
We might want to de-saturate this color::
|
||||
|
||||
>>> c.saturation = 0.5
|
||||
>>> c
|
||||
<Color #bf40bf>
|
||||
|
||||
And of course, the string conversion will give the web representation which is
|
||||
human, or 3-digit, or 6-digit hex representation depending which is usable::
|
||||
|
||||
>>> "%s" % c
|
||||
'#bf40bf'
|
||||
|
||||
>>> c.luminance = 1
|
||||
>>> "%s" % c
|
||||
'white'
|
||||
|
||||
|
||||
Ranges of colors
|
||||
----------------
|
||||
|
||||
You can get some color scale of variation between two ``Color`` objects quite
|
||||
easily. Here, is the color scale of the rainbow between red and blue::
|
||||
|
||||
>>> red = Color("red")
|
||||
>>> blue = Color("blue")
|
||||
>>> list(red.range_to(blue, 5))
|
||||
[<Color red>, <Color yellow>, <Color lime>, <Color cyan>, <Color blue>]
|
||||
|
||||
Or the different amount of gray between black and white::
|
||||
|
||||
>>> black = Color("black")
|
||||
>>> white = Color("white")
|
||||
>>> list(black.range_to(white, 6))
|
||||
[<Color black>, <Color #333>, <Color #666>, <Color #999>, <Color #ccc>, <Color white>]
|
||||
|
||||
|
||||
If you have to create graphical representation with color scale
|
||||
between red and green ('lime' color is full green)::
|
||||
|
||||
>>> lime = Color("lime")
|
||||
>>> list(red.range_to(lime, 5))
|
||||
[<Color red>, <Color #ff7f00>, <Color yellow>, <Color chartreuse>, <Color lime>]
|
||||
|
||||
Notice how naturally, the yellow is displayed in human format and in
|
||||
the middle of the scale. And that the quite unusual (but compatible)
|
||||
'chartreuse' color specification has been used in place of the
|
||||
hexadecimal representation.
|
||||
|
||||
|
||||
Color comparison
|
||||
----------------
|
||||
|
||||
Sane default
|
||||
~~~~~~~~~~~~
|
||||
|
||||
Color comparison is a vast subject. However, it might seem quite straightforward for
|
||||
you. ``Colour`` uses a configurable default way of comparing color that might suit
|
||||
your needs::
|
||||
|
||||
>>> Color("red") == Color("#f00") == Color("blue", hue=0)
|
||||
True
|
||||
|
||||
The default comparison algorithm focuses only on the "web" representation which is
|
||||
equivalent to comparing the long hex representation (e.g. #FF0000) or to be more
|
||||
specific, it is equivalent to compare the amount of red, green, and blue composition
|
||||
of the RGB representation, each of these value being quantized to a 256 value scale.
|
||||
|
||||
This default comparison is a practical and convenient way to measure the actual
|
||||
color equivalence on your screen, or in your video card memory.
|
||||
|
||||
But this comparison wouldn't make the difference between a black red, and a
|
||||
black blue, which both are black::
|
||||
|
||||
>>> black_red = Color("red", luminance=0)
|
||||
>>> black_blue = Color("blue", luminance=0)
|
||||
|
||||
>>> black_red == black_blue
|
||||
True
|
||||
|
||||
|
||||
Customization
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
But, this is not the sole way to compare two colors. As I'm quite lazy, I'm providing
|
||||
you a way to customize it to your needs. Thus::
|
||||
|
||||
>>> from colour import RGB_equivalence, HSL_equivalence
|
||||
>>> black_red = Color("red", luminance=0, equality=HSL_equivalence)
|
||||
>>> black_blue = Color("blue", luminance=0, equality=HSL_equivalence)
|
||||
|
||||
>>> black_red == black_blue
|
||||
False
|
||||
|
||||
As you might have already guessed, the sane default is ``RGB_equivalence``, so::
|
||||
|
||||
>>> black_red = Color("red", luminance=0, equality=RGB_equivalence)
|
||||
>>> black_blue = Color("blue", luminance=0, equality=RGB_equivalence)
|
||||
|
||||
>>> black_red == black_blue
|
||||
True
|
||||
|
||||
Here's how you could implement your unique comparison function::
|
||||
|
||||
>>> saturation_equivalence = lambda c1, c2: c1.saturation == c2.saturation
|
||||
>>> red = Color("red", equality=saturation_equivalence)
|
||||
>>> blue = Color("blue", equality=saturation_equivalence)
|
||||
>>> white = Color("white", equality=saturation_equivalence)
|
||||
|
||||
>>> red == blue
|
||||
True
|
||||
>>> white == red
|
||||
False
|
||||
|
||||
Note: When comparing 2 colors, *only* the equality function *of the first
|
||||
color will be used*. Thus::
|
||||
|
||||
>>> black_red = Color("red", luminance=0, equality=RGB_equivalence)
|
||||
>>> black_blue = Color("blue", luminance=0, equality=HSL_equivalence)
|
||||
|
||||
>>> black_red == black_blue
|
||||
True
|
||||
|
||||
But reverse operation is not equivalent !::
|
||||
|
||||
>>> black_blue == black_red
|
||||
False
|
||||
|
||||
|
||||
Equality to non-Colour objects
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
As a side note, whatever your custom equality function is, it won't be
|
||||
used if you compare to anything else than a ``Colour`` instance::
|
||||
|
||||
>>> red = Color("red", equality=lambda c1, c2: True)
|
||||
>>> blue = Color("blue", equality=lambda c1, c2: True)
|
||||
|
||||
Note that these instances would compare as equal to any other color::
|
||||
|
||||
>>> red == blue
|
||||
True
|
||||
|
||||
But on another non-Colour object::
|
||||
|
||||
>>> red == None
|
||||
False
|
||||
>>> red != None
|
||||
True
|
||||
|
||||
Actually, ``Colour`` instances will, politely enough, leave
|
||||
the other side of the equality have a chance to decide of the output,
|
||||
(by executing its own ``__eq__``), so::
|
||||
|
||||
>>> class OtherColorImplem(object):
|
||||
... def __init__(self, color):
|
||||
... self.color = color
|
||||
... def __eq__(self, other):
|
||||
... return self.color == other.web
|
||||
|
||||
>>> alien_red = OtherColorImplem("red")
|
||||
>>> red == alien_red
|
||||
True
|
||||
>>> blue == alien_red
|
||||
False
|
||||
|
||||
And inequality (using ``__ne__``) are also polite::
|
||||
|
||||
>>> class AnotherColorImplem(OtherColorImplem):
|
||||
... def __ne__(self, other):
|
||||
... return self.color != other.web
|
||||
|
||||
>>> new_alien_red = AnotherColorImplem("red")
|
||||
>>> red != new_alien_red
|
||||
False
|
||||
>>> blue != new_alien_red
|
||||
True
|
||||
|
||||
|
||||
Picking arbitrary color for a python object
|
||||
-------------------------------------------
|
||||
|
||||
Basic Usage
|
||||
~~~~~~~~~~~
|
||||
|
||||
Sometimes, you just want to pick a color for an object in your application
|
||||
often to visually identify this object. Thus, the picked color should be the
|
||||
same for same objects, and different for different object::
|
||||
|
||||
>>> foo = object()
|
||||
>>> bar = object()
|
||||
|
||||
>>> Color(pick_for=foo) # doctest: +ELLIPSIS
|
||||
<Color ...>
|
||||
>>> Color(pick_for=foo) == Color(pick_for=foo)
|
||||
True
|
||||
>>> Color(pick_for=foo) == Color(pick_for=bar)
|
||||
False
|
||||
|
||||
Of course, although there's a tiny probability that different strings yield the
|
||||
same color, most of the time, different inputs will produce different colors.
|
||||
|
||||
Advanced Usage
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
You can customize your color picking algorithm by providing a ``picker``. A
|
||||
``picker`` is a callable that takes an object, and returns something that can
|
||||
be instantiated as a color by ``Color``::
|
||||
|
||||
>>> my_picker = lambda obj: "red" if isinstance(obj, int) else "blue"
|
||||
>>> Color(pick_for=3, picker=my_picker, pick_key=None)
|
||||
<Color red>
|
||||
>>> Color(pick_for="foo", picker=my_picker, pick_key=None)
|
||||
<Color blue>
|
||||
|
||||
You might want to use a particular picker, but enforce how the picker will
|
||||
identify two object as the same (or not). So there's a ``pick_key`` attribute
|
||||
that is provided and defaults as equivalent of ``hash`` method and if hash is
|
||||
not supported by your object, it'll default to the ``str`` of your object salted
|
||||
with the class name.
|
||||
|
||||
Thus::
|
||||
|
||||
>>> class MyObj(str): pass
|
||||
>>> my_obj_color = Color(pick_for=MyObj("foo"))
|
||||
>>> my_str_color = Color(pick_for="foo")
|
||||
>>> my_obj_color == my_str_color
|
||||
False
|
||||
|
||||
Please make sure your object is hashable or "stringable" before using the
|
||||
``RGB_color_picker`` picking mechanism or provide another color picker. Nearly
|
||||
all python object are hashable by default so this shouldn't be an issue (e.g.
|
||||
instances of ``object`` and subclasses are hashable).
|
||||
|
||||
Neither ``hash`` nor ``str`` are perfect solution. So feel free to use
|
||||
``pick_key`` at ``Color`` instantiation time to set your way to identify
|
||||
objects, for instance::
|
||||
|
||||
>>> a = object()
|
||||
>>> b = object()
|
||||
>>> Color(pick_for=a, pick_key=id) == Color(pick_for=b, pick_key=id)
|
||||
False
|
||||
|
||||
When choosing a pick key, you should closely consider if you want your color
|
||||
to be consistent between runs (this is NOT the case with the last example),
|
||||
or consistent with the content of your object if it is a mutable object.
|
||||
|
||||
Default value of ``pick_key`` and ``picker`` ensures that the same color will
|
||||
be attributed to same object between different run on different computer for
|
||||
most python object.
|
||||
|
||||
|
||||
Color factory
|
||||
-------------
|
||||
|
||||
As you might have noticed, there are few attributes that you might want to see
|
||||
attached to all of your colors as ``equality`` for equality comparison support,
|
||||
or ``picker``, ``pick_key`` to configure your object color picker.
|
||||
|
||||
You can create a customized ``Color`` factory thanks to the ``make_color_factory``::
|
||||
|
||||
>>> from colour import make_color_factory, HSL_equivalence, RGB_color_picker
|
||||
|
||||
>>> get_color = make_color_factory(
|
||||
... equality=HSL_equivalence,
|
||||
... picker=RGB_color_picker,
|
||||
... pick_key=str,
|
||||
... )
|
||||
|
||||
All color created thanks to ``CustomColor`` class instead of the default one
|
||||
would get the specified attributes by default::
|
||||
|
||||
>>> black_red = get_color("red", luminance=0)
|
||||
>>> black_blue = get_color("blue", luminance=0)
|
||||
|
||||
Of course, these are always instances of ``Color`` class::
|
||||
|
||||
>>> isinstance(black_red, Color)
|
||||
True
|
||||
|
||||
Equality was changed from normal defaults, so::
|
||||
|
||||
>>> black_red == black_blue
|
||||
False
|
||||
|
||||
This because the default equivalence of ``Color`` was set to
|
||||
``HSL_equivalence``.
|
||||
|
||||
|
||||
Contributing
|
||||
============
|
||||
|
||||
Any suggestion or issue is welcome. Push request are very welcome,
|
||||
please check out the guidelines.
|
||||
|
||||
|
||||
Push Request Guidelines
|
||||
-----------------------
|
||||
|
||||
You can send any code. I'll look at it and will integrate it myself in
|
||||
the code base and leave you as the author. This process can take time and
|
||||
it'll take less time if you follow the following guidelines:
|
||||
|
||||
- check your code with PEP8 or pylint. Try to stick to 80 columns wide.
|
||||
- separate your commits per smallest concern.
|
||||
- each commit should pass the tests (to allow easy bisect)
|
||||
- each functionality/bugfix commit should contain the code, tests,
|
||||
and doc.
|
||||
- prior minor commit with typographic or code cosmetic changes are
|
||||
very welcome. These should be tagged in their commit summary with
|
||||
``!minor``.
|
||||
- the commit message should follow gitchangelog rules (check the git
|
||||
log to get examples)
|
||||
- if the commit fixes an issue or finished the implementation of a
|
||||
feature, please mention it in the summary.
|
||||
|
||||
If you have some questions about guidelines which is not answered here,
|
||||
please check the current ``git log``, you might find previous commit that
|
||||
would show you how to deal with your issue.
|
||||
|
||||
|
||||
License
|
||||
=======
|
||||
|
||||
Copyright (c) 2012-2017 Valentin Lab.
|
||||
|
||||
Licensed under the `BSD License`_.
|
||||
|
||||
.. _BSD License: http://raw.github.com/vaab/colour/master/LICENSE
|
||||
|
||||
Changelog
|
||||
=========
|
||||
|
||||
|
||||
0.1.4 (2017-04-19)
|
||||
------------------
|
||||
|
||||
Fix
|
||||
~~~
|
||||
- ``rgb2hsl`` would produce invalid hsl triplet when red, blue, green
|
||||
component would be all very close to ``1.0``. (fixes #30) [Valentin
|
||||
Lab]
|
||||
|
||||
Typically, saturation would shoot out of range 0.0..1.0. That could then
|
||||
lead to exceptions being casts afterwards when trying to reconvert this
|
||||
HSL triplet to RGB values.
|
||||
|
||||
|
||||
0.1.3 (2017-04-08)
|
||||
------------------
|
||||
|
||||
Fix
|
||||
~~~
|
||||
- Unexpected behavior with ``!=`` operator. (fixes #26) [Valentin Lab]
|
||||
- Added mention of the ``hex_l`` property. (fixes #27) [Valentin Lab]
|
||||
|
||||
|
||||
0.1.2 (2015-09-15)
|
||||
------------------
|
||||
|
||||
Fix
|
||||
~~~
|
||||
- Support for corner case 1-wide ``range_to`` color scale. (fixes #18)
|
||||
[Valentin Lab]
|
||||
|
||||
|
||||
0.1.1 (2015-03-29)
|
||||
------------------
|
||||
|
||||
Fix
|
||||
~~~
|
||||
- Avoid casting an exception when comparing to non-``Colour`` instances.
|
||||
(fixes #14) [Riziq Sayegh]
|
||||
|
||||
|
||||
0.0.6 (2014-11-18)
|
||||
------------------
|
||||
|
||||
New
|
||||
~~~
|
||||
- Provide all missing *2* function by combination with other existing
|
||||
ones (fixes #13). [Valentin Lab]
|
||||
- Provide full access to any color name in HSL, RGB, HEX convenience
|
||||
instances. [Valentin Lab]
|
||||
|
||||
Now you can call ``colour.HSL.cyan``, or ``colour.HEX.red`` for a direct encoding of
|
||||
``human`` colour labels to the 3 representations.
|
||||
|
||||
|
||||
0.0.5 (2013-09-16)
|
||||
------------------
|
||||
|
||||
New
|
||||
~~~
|
||||
- Color names are case insensitive. [Chris Priest]
|
||||
|
||||
The color-name structure have their names capitalized. And color names
|
||||
that are made of only one word will be displayed lowercased.
|
||||
|
||||
Fix
|
||||
~~~
|
||||
- Now using W3C color recommandation. [Chris Priest]
|
||||
|
||||
Was using X11 color scheme before, which is slightly different from
|
||||
W3C web color specifications.
|
||||
- Inconsistency in licence information (removed GPL mention). (fixes #8)
|
||||
[Valentin Lab]
|
||||
- Removed ``gitchangelog`` from ``setup.py`` require list. (fixes #9)
|
||||
[Valentin Lab]
|
||||
|
||||
|
||||
0.0.4 (2013-06-21)
|
||||
------------------
|
||||
|
||||
New
|
||||
~~~
|
||||
- Added ``make_color_factory`` to customize some common color
|
||||
attributes. [Valentin Lab]
|
||||
- Pick color to identify any python object (fixes #6) [Jonathan Ballet]
|
||||
- Equality support between colors, customizable if needed. (fixes #3)
|
||||
[Valentin Lab]
|
||||
|
||||
|
||||
0.0.3 (2013-06-19)
|
||||
------------------
|
||||
|
||||
New
|
||||
~~~
|
||||
- Colour is now compatible with python3. [Ryan Leckey]
|
||||
|
||||
|
||||
0.0.1 (2012-06-11)
|
||||
------------------
|
||||
- First import. [Valentin Lab]
|
||||
|
||||
TODO
|
||||
====
|
||||
|
||||
- ANSI 16-color and 256-color escape sequence generation
|
||||
- YUV, HSV, CMYK support
|
||||
|
||||
|
||||
|
@ -1 +0,0 @@
|
||||
pip
|
@ -1,23 +0,0 @@
|
||||
Copyright (c) 2012-2017, Valentin Lab
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
@ -1,630 +0,0 @@
|
||||
Metadata-Version: 2.0
|
||||
Name: colour
|
||||
Version: 0.1.5
|
||||
Summary: converts and manipulates various color representation (HSL, RVB, web, X11, ...)
|
||||
Home-page: http://github.com/vaab/colour
|
||||
Author: Valentin LAB
|
||||
Author-email: valentin.lab@kalysto.org
|
||||
License: BSD 3-Clause License
|
||||
Platform: UNKNOWN
|
||||
Classifier: Programming Language :: Python
|
||||
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
||||
Classifier: Development Status :: 3 - Alpha
|
||||
Classifier: License :: OSI Approved :: BSD License
|
||||
Classifier: Intended Audience :: Developers
|
||||
Classifier: Programming Language :: Python :: 2
|
||||
Classifier: Programming Language :: Python :: 2.7
|
||||
Classifier: Programming Language :: Python :: 3
|
||||
Classifier: Programming Language :: Python :: 3.3
|
||||
Classifier: Programming Language :: Python :: 3.4
|
||||
Classifier: Programming Language :: Python :: 3.5
|
||||
Classifier: Programming Language :: Python :: 3.6
|
||||
Provides-Extra: test
|
||||
Requires-Dist: nose; extra == 'test'
|
||||
|
||||
======
|
||||
Colour
|
||||
======
|
||||
|
||||
.. image:: http://img.shields.io/pypi/v/colour.svg?style=flat
|
||||
:target: https://pypi.python.org/pypi/colour/
|
||||
:alt: Latest PyPI version
|
||||
|
||||
.. image:: https://img.shields.io/pypi/l/gitchangelog.svg?style=flat
|
||||
:target: https://github.com/vaab/gitchangelog/blob/master/LICENSE
|
||||
:alt: License
|
||||
|
||||
.. image:: https://img.shields.io/pypi/pyversions/gitchangelog.svg?style=flat
|
||||
:target: https://pypi.python.org/pypi/gitchangelog/
|
||||
:alt: Compatible python versions
|
||||
|
||||
.. image:: http://img.shields.io/pypi/dm/colour.svg?style=flat
|
||||
:target: https://pypi.python.org/pypi/colour/
|
||||
:alt: Number of PyPI downloads
|
||||
|
||||
.. image:: http://img.shields.io/travis/vaab/colour/master.svg?style=flat
|
||||
:target: https://travis-ci.org/vaab/colour/
|
||||
:alt: Travis CI build status
|
||||
|
||||
.. image:: https://img.shields.io/appveyor/ci/vaab/colour.svg
|
||||
:target: https://ci.appveyor.com/project/vaab/colour/branch/master
|
||||
:alt: Appveyor CI build status
|
||||
|
||||
.. image:: http://img.shields.io/codecov/c/github/vaab/colour.svg?style=flat
|
||||
:target: https://codecov.io/gh/vaab/colour/
|
||||
:alt: Test coverage
|
||||
|
||||
|
||||
Converts and manipulates common color representation (RGB, HSL, web, ...)
|
||||
|
||||
|
||||
Feature
|
||||
=======
|
||||
|
||||
- Damn simple and pythonic way to manipulate color representation (see
|
||||
examples below)
|
||||
|
||||
- Full conversion between RGB, HSL, 6-digit hex, 3-digit hex, human color
|
||||
|
||||
- One object (``Color``) or bunch of single purpose function (``rgb2hex``,
|
||||
``hsl2rgb`` ...)
|
||||
|
||||
- ``web`` format that use the smallest representation between
|
||||
6-digit (e.g. ``#fa3b2c``), 3-digit (e.g. ``#fbb``), fully spelled
|
||||
color (e.g. ``white``), following `W3C color naming`_ for compatible
|
||||
CSS or HTML color specifications.
|
||||
|
||||
- smooth intuitive color scale generation choosing N color gradients.
|
||||
|
||||
- can pick colors for you to identify objects of your application.
|
||||
|
||||
|
||||
.. _W3C color naming: http://www.w3.org/TR/css3-color/#svg-color
|
||||
|
||||
|
||||
Installation
|
||||
============
|
||||
|
||||
You don't need to download the GIT version of the code as ``colour`` is
|
||||
available on the PyPI. So you should be able to run::
|
||||
|
||||
pip install colour
|
||||
|
||||
If you have downloaded the GIT sources, then you could add the ``colour.py``
|
||||
directly to one of your ``site-packages`` (thanks to a symlink). Or install
|
||||
the current version via traditional::
|
||||
|
||||
python setup.py install
|
||||
|
||||
And if you don't have the GIT sources but would like to get the latest
|
||||
master or branch from github, you could also::
|
||||
|
||||
pip install git+https://github.com/vaab/colour
|
||||
|
||||
Or even select a specific revision (branch/tag/commit)::
|
||||
|
||||
pip install git+https://github.com/vaab/colour@master
|
||||
|
||||
|
||||
Usage
|
||||
=====
|
||||
|
||||
To get complete demo of each function, please read the source code which is
|
||||
heavily documented and provide a lot of examples in doctest format.
|
||||
|
||||
Here is a reduced sample of a common usage scenario:
|
||||
|
||||
|
||||
Instantiation
|
||||
-------------
|
||||
|
||||
Let's create blue color::
|
||||
|
||||
>>> from colour import Color
|
||||
>>> c = Color("blue")
|
||||
>>> c
|
||||
<Color blue>
|
||||
|
||||
Please note that all of these are equivalent examples to create the red color::
|
||||
|
||||
Color("red") ## human, web compatible representation
|
||||
Color(red=1) ## default amount of blue and green is 0.0
|
||||
Color("blue", hue=0) ## hue of blue is 0.66, hue of red is 0.0
|
||||
Color("#f00") ## standard 3 hex digit web compatible representation
|
||||
Color("#ff0000") ## standard 6 hex digit web compatible representation
|
||||
Color(hue=0, saturation=1, luminance=0.5)
|
||||
Color(hsl=(0, 1, 0.5)) ## full 3-uple HSL specification
|
||||
Color(rgb=(1, 0, 0)) ## full 3-uple RGB specification
|
||||
Color(Color("red")) ## recursion doesn't break object
|
||||
|
||||
|
||||
Reading values
|
||||
--------------
|
||||
|
||||
Several representations are accessible::
|
||||
|
||||
>>> c.hex
|
||||
'#00f'
|
||||
>>> c.hsl # doctest: +ELLIPSIS
|
||||
(0.66..., 1.0, 0.5)
|
||||
>>> c.rgb
|
||||
(0.0, 0.0, 1.0)
|
||||
|
||||
And their different parts are also independently accessible, as the different
|
||||
amount of red, blue, green, in the RGB format::
|
||||
|
||||
>>> c.red
|
||||
0.0
|
||||
>>> c.blue
|
||||
1.0
|
||||
>>> c.green
|
||||
0.0
|
||||
|
||||
Or the hue, saturation and luminance of the HSL representation::
|
||||
|
||||
>>> c.hue # doctest: +ELLIPSIS
|
||||
0.66...
|
||||
>>> c.saturation
|
||||
1.0
|
||||
>>> c.luminance
|
||||
0.5
|
||||
|
||||
A note on the ``.hex`` property, it'll return the smallest valid value
|
||||
when possible. If you are only interested by the long value, use
|
||||
``.hex_l``::
|
||||
|
||||
>>> c.hex_l
|
||||
'#0000ff'
|
||||
|
||||
|
||||
Modifying color objects
|
||||
-----------------------
|
||||
|
||||
All of these properties are read/write, so let's add some red to this color::
|
||||
|
||||
>>> c.red = 1
|
||||
>>> c
|
||||
<Color magenta>
|
||||
|
||||
We might want to de-saturate this color::
|
||||
|
||||
>>> c.saturation = 0.5
|
||||
>>> c
|
||||
<Color #bf40bf>
|
||||
|
||||
And of course, the string conversion will give the web representation which is
|
||||
human, or 3-digit, or 6-digit hex representation depending which is usable::
|
||||
|
||||
>>> "%s" % c
|
||||
'#bf40bf'
|
||||
|
||||
>>> c.luminance = 1
|
||||
>>> "%s" % c
|
||||
'white'
|
||||
|
||||
|
||||
Ranges of colors
|
||||
----------------
|
||||
|
||||
You can get some color scale of variation between two ``Color`` objects quite
|
||||
easily. Here, is the color scale of the rainbow between red and blue::
|
||||
|
||||
>>> red = Color("red")
|
||||
>>> blue = Color("blue")
|
||||
>>> list(red.range_to(blue, 5))
|
||||
[<Color red>, <Color yellow>, <Color lime>, <Color cyan>, <Color blue>]
|
||||
|
||||
Or the different amount of gray between black and white::
|
||||
|
||||
>>> black = Color("black")
|
||||
>>> white = Color("white")
|
||||
>>> list(black.range_to(white, 6))
|
||||
[<Color black>, <Color #333>, <Color #666>, <Color #999>, <Color #ccc>, <Color white>]
|
||||
|
||||
|
||||
If you have to create graphical representation with color scale
|
||||
between red and green ('lime' color is full green)::
|
||||
|
||||
>>> lime = Color("lime")
|
||||
>>> list(red.range_to(lime, 5))
|
||||
[<Color red>, <Color #ff7f00>, <Color yellow>, <Color chartreuse>, <Color lime>]
|
||||
|
||||
Notice how naturally, the yellow is displayed in human format and in
|
||||
the middle of the scale. And that the quite unusual (but compatible)
|
||||
'chartreuse' color specification has been used in place of the
|
||||
hexadecimal representation.
|
||||
|
||||
|
||||
Color comparison
|
||||
----------------
|
||||
|
||||
Sane default
|
||||
~~~~~~~~~~~~
|
||||
|
||||
Color comparison is a vast subject. However, it might seem quite straightforward for
|
||||
you. ``Colour`` uses a configurable default way of comparing color that might suit
|
||||
your needs::
|
||||
|
||||
>>> Color("red") == Color("#f00") == Color("blue", hue=0)
|
||||
True
|
||||
|
||||
The default comparison algorithm focuses only on the "web" representation which is
|
||||
equivalent to comparing the long hex representation (e.g. #FF0000) or to be more
|
||||
specific, it is equivalent to compare the amount of red, green, and blue composition
|
||||
of the RGB representation, each of these value being quantized to a 256 value scale.
|
||||
|
||||
This default comparison is a practical and convenient way to measure the actual
|
||||
color equivalence on your screen, or in your video card memory.
|
||||
|
||||
But this comparison wouldn't make the difference between a black red, and a
|
||||
black blue, which both are black::
|
||||
|
||||
>>> black_red = Color("red", luminance=0)
|
||||
>>> black_blue = Color("blue", luminance=0)
|
||||
|
||||
>>> black_red == black_blue
|
||||
True
|
||||
|
||||
|
||||
Customization
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
But, this is not the sole way to compare two colors. As I'm quite lazy, I'm providing
|
||||
you a way to customize it to your needs. Thus::
|
||||
|
||||
>>> from colour import RGB_equivalence, HSL_equivalence
|
||||
>>> black_red = Color("red", luminance=0, equality=HSL_equivalence)
|
||||
>>> black_blue = Color("blue", luminance=0, equality=HSL_equivalence)
|
||||
|
||||
>>> black_red == black_blue
|
||||
False
|
||||
|
||||
As you might have already guessed, the sane default is ``RGB_equivalence``, so::
|
||||
|
||||
>>> black_red = Color("red", luminance=0, equality=RGB_equivalence)
|
||||
>>> black_blue = Color("blue", luminance=0, equality=RGB_equivalence)
|
||||
|
||||
>>> black_red == black_blue
|
||||
True
|
||||
|
||||
Here's how you could implement your unique comparison function::
|
||||
|
||||
>>> saturation_equivalence = lambda c1, c2: c1.saturation == c2.saturation
|
||||
>>> red = Color("red", equality=saturation_equivalence)
|
||||
>>> blue = Color("blue", equality=saturation_equivalence)
|
||||
>>> white = Color("white", equality=saturation_equivalence)
|
||||
|
||||
>>> red == blue
|
||||
True
|
||||
>>> white == red
|
||||
False
|
||||
|
||||
Note: When comparing 2 colors, *only* the equality function *of the first
|
||||
color will be used*. Thus::
|
||||
|
||||
>>> black_red = Color("red", luminance=0, equality=RGB_equivalence)
|
||||
>>> black_blue = Color("blue", luminance=0, equality=HSL_equivalence)
|
||||
|
||||
>>> black_red == black_blue
|
||||
True
|
||||
|
||||
But reverse operation is not equivalent !::
|
||||
|
||||
>>> black_blue == black_red
|
||||
False
|
||||
|
||||
|
||||
Equality to non-Colour objects
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
As a side note, whatever your custom equality function is, it won't be
|
||||
used if you compare to anything else than a ``Colour`` instance::
|
||||
|
||||
>>> red = Color("red", equality=lambda c1, c2: True)
|
||||
>>> blue = Color("blue", equality=lambda c1, c2: True)
|
||||
|
||||
Note that these instances would compare as equal to any other color::
|
||||
|
||||
>>> red == blue
|
||||
True
|
||||
|
||||
But on another non-Colour object::
|
||||
|
||||
>>> red == None
|
||||
False
|
||||
>>> red != None
|
||||
True
|
||||
|
||||
Actually, ``Colour`` instances will, politely enough, leave
|
||||
the other side of the equality have a chance to decide of the output,
|
||||
(by executing its own ``__eq__``), so::
|
||||
|
||||
>>> class OtherColorImplem(object):
|
||||
... def __init__(self, color):
|
||||
... self.color = color
|
||||
... def __eq__(self, other):
|
||||
... return self.color == other.web
|
||||
|
||||
>>> alien_red = OtherColorImplem("red")
|
||||
>>> red == alien_red
|
||||
True
|
||||
>>> blue == alien_red
|
||||
False
|
||||
|
||||
And inequality (using ``__ne__``) are also polite::
|
||||
|
||||
>>> class AnotherColorImplem(OtherColorImplem):
|
||||
... def __ne__(self, other):
|
||||
... return self.color != other.web
|
||||
|
||||
>>> new_alien_red = AnotherColorImplem("red")
|
||||
>>> red != new_alien_red
|
||||
False
|
||||
>>> blue != new_alien_red
|
||||
True
|
||||
|
||||
|
||||
Picking arbitrary color for a python object
|
||||
-------------------------------------------
|
||||
|
||||
Basic Usage
|
||||
~~~~~~~~~~~
|
||||
|
||||
Sometimes, you just want to pick a color for an object in your application
|
||||
often to visually identify this object. Thus, the picked color should be the
|
||||
same for same objects, and different for different object::
|
||||
|
||||
>>> foo = object()
|
||||
>>> bar = object()
|
||||
|
||||
>>> Color(pick_for=foo) # doctest: +ELLIPSIS
|
||||
<Color ...>
|
||||
>>> Color(pick_for=foo) == Color(pick_for=foo)
|
||||
True
|
||||
>>> Color(pick_for=foo) == Color(pick_for=bar)
|
||||
False
|
||||
|
||||
Of course, although there's a tiny probability that different strings yield the
|
||||
same color, most of the time, different inputs will produce different colors.
|
||||
|
||||
Advanced Usage
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
You can customize your color picking algorithm by providing a ``picker``. A
|
||||
``picker`` is a callable that takes an object, and returns something that can
|
||||
be instantiated as a color by ``Color``::
|
||||
|
||||
>>> my_picker = lambda obj: "red" if isinstance(obj, int) else "blue"
|
||||
>>> Color(pick_for=3, picker=my_picker, pick_key=None)
|
||||
<Color red>
|
||||
>>> Color(pick_for="foo", picker=my_picker, pick_key=None)
|
||||
<Color blue>
|
||||
|
||||
You might want to use a particular picker, but enforce how the picker will
|
||||
identify two object as the same (or not). So there's a ``pick_key`` attribute
|
||||
that is provided and defaults as equivalent of ``hash`` method and if hash is
|
||||
not supported by your object, it'll default to the ``str`` of your object salted
|
||||
with the class name.
|
||||
|
||||
Thus::
|
||||
|
||||
>>> class MyObj(str): pass
|
||||
>>> my_obj_color = Color(pick_for=MyObj("foo"))
|
||||
>>> my_str_color = Color(pick_for="foo")
|
||||
>>> my_obj_color == my_str_color
|
||||
False
|
||||
|
||||
Please make sure your object is hashable or "stringable" before using the
|
||||
``RGB_color_picker`` picking mechanism or provide another color picker. Nearly
|
||||
all python object are hashable by default so this shouldn't be an issue (e.g.
|
||||
instances of ``object`` and subclasses are hashable).
|
||||
|
||||
Neither ``hash`` nor ``str`` are perfect solution. So feel free to use
|
||||
``pick_key`` at ``Color`` instantiation time to set your way to identify
|
||||
objects, for instance::
|
||||
|
||||
>>> a = object()
|
||||
>>> b = object()
|
||||
>>> Color(pick_for=a, pick_key=id) == Color(pick_for=b, pick_key=id)
|
||||
False
|
||||
|
||||
When choosing a pick key, you should closely consider if you want your color
|
||||
to be consistent between runs (this is NOT the case with the last example),
|
||||
or consistent with the content of your object if it is a mutable object.
|
||||
|
||||
Default value of ``pick_key`` and ``picker`` ensures that the same color will
|
||||
be attributed to same object between different run on different computer for
|
||||
most python object.
|
||||
|
||||
|
||||
Color factory
|
||||
-------------
|
||||
|
||||
As you might have noticed, there are few attributes that you might want to see
|
||||
attached to all of your colors as ``equality`` for equality comparison support,
|
||||
or ``picker``, ``pick_key`` to configure your object color picker.
|
||||
|
||||
You can create a customized ``Color`` factory thanks to the ``make_color_factory``::
|
||||
|
||||
>>> from colour import make_color_factory, HSL_equivalence, RGB_color_picker
|
||||
|
||||
>>> get_color = make_color_factory(
|
||||
... equality=HSL_equivalence,
|
||||
... picker=RGB_color_picker,
|
||||
... pick_key=str,
|
||||
... )
|
||||
|
||||
All color created thanks to ``CustomColor`` class instead of the default one
|
||||
would get the specified attributes by default::
|
||||
|
||||
>>> black_red = get_color("red", luminance=0)
|
||||
>>> black_blue = get_color("blue", luminance=0)
|
||||
|
||||
Of course, these are always instances of ``Color`` class::
|
||||
|
||||
>>> isinstance(black_red, Color)
|
||||
True
|
||||
|
||||
Equality was changed from normal defaults, so::
|
||||
|
||||
>>> black_red == black_blue
|
||||
False
|
||||
|
||||
This because the default equivalence of ``Color`` was set to
|
||||
``HSL_equivalence``.
|
||||
|
||||
|
||||
Contributing
|
||||
============
|
||||
|
||||
Any suggestion or issue is welcome. Push request are very welcome,
|
||||
please check out the guidelines.
|
||||
|
||||
|
||||
Push Request Guidelines
|
||||
-----------------------
|
||||
|
||||
You can send any code. I'll look at it and will integrate it myself in
|
||||
the code base and leave you as the author. This process can take time and
|
||||
it'll take less time if you follow the following guidelines:
|
||||
|
||||
- check your code with PEP8 or pylint. Try to stick to 80 columns wide.
|
||||
- separate your commits per smallest concern.
|
||||
- each commit should pass the tests (to allow easy bisect)
|
||||
- each functionality/bugfix commit should contain the code, tests,
|
||||
and doc.
|
||||
- prior minor commit with typographic or code cosmetic changes are
|
||||
very welcome. These should be tagged in their commit summary with
|
||||
``!minor``.
|
||||
- the commit message should follow gitchangelog rules (check the git
|
||||
log to get examples)
|
||||
- if the commit fixes an issue or finished the implementation of a
|
||||
feature, please mention it in the summary.
|
||||
|
||||
If you have some questions about guidelines which is not answered here,
|
||||
please check the current ``git log``, you might find previous commit that
|
||||
would show you how to deal with your issue.
|
||||
|
||||
|
||||
License
|
||||
=======
|
||||
|
||||
Copyright (c) 2012-2017 Valentin Lab.
|
||||
|
||||
Licensed under the `BSD License`_.
|
||||
|
||||
.. _BSD License: http://raw.github.com/vaab/colour/master/LICENSE
|
||||
|
||||
Changelog
|
||||
=========
|
||||
|
||||
|
||||
0.1.4 (2017-04-19)
|
||||
------------------
|
||||
|
||||
Fix
|
||||
~~~
|
||||
- ``rgb2hsl`` would produce invalid hsl triplet when red, blue, green
|
||||
component would be all very close to ``1.0``. (fixes #30) [Valentin
|
||||
Lab]
|
||||
|
||||
Typically, saturation would shoot out of range 0.0..1.0. That could then
|
||||
lead to exceptions being casts afterwards when trying to reconvert this
|
||||
HSL triplet to RGB values.
|
||||
|
||||
|
||||
0.1.3 (2017-04-08)
|
||||
------------------
|
||||
|
||||
Fix
|
||||
~~~
|
||||
- Unexpected behavior with ``!=`` operator. (fixes #26) [Valentin Lab]
|
||||
- Added mention of the ``hex_l`` property. (fixes #27) [Valentin Lab]
|
||||
|
||||
|
||||
0.1.2 (2015-09-15)
|
||||
------------------
|
||||
|
||||
Fix
|
||||
~~~
|
||||
- Support for corner case 1-wide ``range_to`` color scale. (fixes #18)
|
||||
[Valentin Lab]
|
||||
|
||||
|
||||
0.1.1 (2015-03-29)
|
||||
------------------
|
||||
|
||||
Fix
|
||||
~~~
|
||||
- Avoid casting an exception when comparing to non-``Colour`` instances.
|
||||
(fixes #14) [Riziq Sayegh]
|
||||
|
||||
|
||||
0.0.6 (2014-11-18)
|
||||
------------------
|
||||
|
||||
New
|
||||
~~~
|
||||
- Provide all missing *2* function by combination with other existing
|
||||
ones (fixes #13). [Valentin Lab]
|
||||
- Provide full access to any color name in HSL, RGB, HEX convenience
|
||||
instances. [Valentin Lab]
|
||||
|
||||
Now you can call ``colour.HSL.cyan``, or ``colour.HEX.red`` for a direct encoding of
|
||||
``human`` colour labels to the 3 representations.
|
||||
|
||||
|
||||
0.0.5 (2013-09-16)
|
||||
------------------
|
||||
|
||||
New
|
||||
~~~
|
||||
- Color names are case insensitive. [Chris Priest]
|
||||
|
||||
The color-name structure have their names capitalized. And color names
|
||||
that are made of only one word will be displayed lowercased.
|
||||
|
||||
Fix
|
||||
~~~
|
||||
- Now using W3C color recommandation. [Chris Priest]
|
||||
|
||||
Was using X11 color scheme before, which is slightly different from
|
||||
W3C web color specifications.
|
||||
- Inconsistency in licence information (removed GPL mention). (fixes #8)
|
||||
[Valentin Lab]
|
||||
- Removed ``gitchangelog`` from ``setup.py`` require list. (fixes #9)
|
||||
[Valentin Lab]
|
||||
|
||||
|
||||
0.0.4 (2013-06-21)
|
||||
------------------
|
||||
|
||||
New
|
||||
~~~
|
||||
- Added ``make_color_factory`` to customize some common color
|
||||
attributes. [Valentin Lab]
|
||||
- Pick color to identify any python object (fixes #6) [Jonathan Ballet]
|
||||
- Equality support between colors, customizable if needed. (fixes #3)
|
||||
[Valentin Lab]
|
||||
|
||||
|
||||
0.0.3 (2013-06-19)
|
||||
------------------
|
||||
|
||||
New
|
||||
~~~
|
||||
- Colour is now compatible with python3. [Ryan Leckey]
|
||||
|
||||
|
||||
0.0.1 (2012-06-11)
|
||||
------------------
|
||||
- First import. [Valentin Lab]
|
||||
|
||||
TODO
|
||||
====
|
||||
|
||||
- ANSI 16-color and 256-color escape sequence generation
|
||||
- YUV, HSV, CMYK support
|
||||
|
||||
|
||||
|
@ -1,11 +0,0 @@
|
||||
colour-0.1.5.dist-info/DESCRIPTION.rst,sha256=hPBkXALLft1zfCftLr--oylXrhnxFIXvRVk3ga--bK8,17325
|
||||
colour-0.1.5.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||
colour-0.1.5.dist-info/LICENSE.txt,sha256=jwhqMkd7-dP7p3VMptM6CpLlsx0DZfmEdg3GigdOQng,1304
|
||||
colour-0.1.5.dist-info/METADATA,sha256=1dIO3yNuvoAan1hBE9_y766TbXs9cKRjEuIUI92nUhE,18273
|
||||
colour-0.1.5.dist-info/RECORD,,
|
||||
colour-0.1.5.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
colour-0.1.5.dist-info/WHEEL,sha256=o2k-Qa-RMNIJmUdIc7KU6VWR_ErNRbWNlxDIpl7lm34,110
|
||||
colour-0.1.5.dist-info/metadata.json,sha256=OviC7TmmKwtRUjauJE5IrjD9kyga_JzrCUZQri6L1xo,1090
|
||||
colour-0.1.5.dist-info/top_level.txt,sha256=XKC-SRGJZMW4pzXxl1NWLlZYzRFz7JuWiOJSkQfrWOE,7
|
||||
colour.py,sha256=GNhLVa29gX2sIT4h79BbnyEJeZDa7uSRt49Cz1zJL_U,28693
|
||||
colour.pyc,,
|
@ -1,6 +0,0 @@
|
||||
Wheel-Version: 1.0
|
||||
Generator: bdist_wheel (0.29.0)
|
||||
Root-Is-Purelib: true
|
||||
Tag: py2-none-any
|
||||
Tag: py3-none-any
|
||||
|
@ -1 +0,0 @@
|
||||
{"classifiers": ["Programming Language :: Python", "Topic :: Software Development :: Libraries :: Python Modules", "Development Status :: 3 - Alpha", "License :: OSI Approved :: BSD License", "Intended Audience :: Developers", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6"], "extensions": {"python.details": {"contacts": [{"email": "valentin.lab@kalysto.org", "name": "Valentin LAB", "role": "author"}], "document_names": {"description": "DESCRIPTION.rst", "license": "LICENSE.txt"}, "project_urls": {"Home": "http://github.com/vaab/colour"}}}, "extras": ["test"], "generator": "bdist_wheel (0.29.0)", "license": "BSD 3-Clause License", "metadata_version": "2.0", "name": "colour", "run_requires": [{"extra": "test", "requires": ["nose"]}], "summary": "converts and manipulates various color representation (HSL, RVB, web, X11, ...)", "version": "0.1.5"}
|
@ -1 +0,0 @@
|
||||
colour
|
File diff suppressed because it is too large
Load Diff
@ -1 +0,0 @@
|
||||
pip
|
@ -1,18 +0,0 @@
|
||||
Metadata-Version: 2.1
|
||||
Name: deezeridu
|
||||
Version: 0.0.2
|
||||
Summary: Downloads songs, albums or playlists from deezer
|
||||
Home-page: https://github.com/OpenJarbas/deezeridu
|
||||
Author: An0nimia
|
||||
License: CC BY-NC-SA 4.0
|
||||
Platform: UNKNOWN
|
||||
Requires-Python: >=3.8
|
||||
Description-Content-Type: text/markdown
|
||||
Requires-Dist: mutagen
|
||||
Requires-Dist: pycryptodomex
|
||||
Requires-Dist: requests
|
||||
Requires-Dist: tqdm
|
||||
Requires-Dist: json-database (>=0.5.6)
|
||||
|
||||
UNKNOWN
|
||||
|
@ -1,34 +0,0 @@
|
||||
deezeridu-0.0.2.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||
deezeridu-0.0.2.dist-info/METADATA,sha256=JBjeAZnVtVZvYgmfElDjDUGdS9EjEwqxaiJGE541aH8,430
|
||||
deezeridu-0.0.2.dist-info/RECORD,,
|
||||
deezeridu-0.0.2.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
deezeridu-0.0.2.dist-info/WHEEL,sha256=g4nMs7d-Xl9-xC9XovUrsDHGXt-FT0E17Yqo92DEfvY,92
|
||||
deezeridu-0.0.2.dist-info/top_level.txt,sha256=5OOrodHgvewBBzVAiYzCWOzPv55J_lxWSYD_H86uoaM,27
|
||||
deezeridu/__init__.py,sha256=JLrWou6ghlPaCCvudqUMt0WLXNAYcLMkgd6ALyff4xY,9462
|
||||
deezeridu/__init__.pyc,,
|
||||
deezeridu/api.py,sha256=YRdBnqu4KMgDFyXM1Zutlkux-tREtq10-tkPdssB8Tc,6116
|
||||
deezeridu/api.pyc,,
|
||||
deezeridu/download.py,sha256=RH9JgXWiKMx3vWeAOL44kG4AjmTUHnaWwAG88_isdCs,16473
|
||||
deezeridu/download.pyc,,
|
||||
deezeridu/download_utils.py,sha256=fpJJvy5WCYR3jeN23yLZ73_nametlWNUTH5kEePAGeQ,1850
|
||||
deezeridu/download_utils.pyc,,
|
||||
deezeridu/exceptions.py,sha256=FyXJME7bFs0MpksfHDtbStdegMyTu3I5_V_ttPYsCmA,2152
|
||||
deezeridu/exceptions.pyc,,
|
||||
deezeridu/gateway.py,sha256=Hv2gqK3qaKFMiJIPbHF98eTbKU4WL_zMSuuxOR0DhGY,7321
|
||||
deezeridu/gateway.pyc,,
|
||||
deezeridu/models/__init__.py,sha256=MKWdOOYUJVhWjWAUvk7wEm5jC88QCRIFK5oHbDePIKw,138
|
||||
deezeridu/models/__init__.pyc,,
|
||||
deezeridu/models/album.py,sha256=mcaZDyZQNhpPyNpHzkYZt05u0oZJLJFiOIDBoHM_vnI,504
|
||||
deezeridu/models/album.pyc,,
|
||||
deezeridu/models/playlist.py,sha256=z37DLc6-lQiXthtJz0kAwxTG3wlc8ZosiL9aRo0VpLk,220
|
||||
deezeridu/models/playlist.pyc,,
|
||||
deezeridu/models/preferences.py,sha256=UOl-6T9tgXueV_kG5qboF_G4I-Znrne6ctZ8km9OoXw,424
|
||||
deezeridu/models/preferences.pyc,,
|
||||
deezeridu/models/track.py,sha256=ePJ60dobaI141N-BtIP83jUr8MxOgTU1xyQ665w5rVQ,1644
|
||||
deezeridu/models/track.pyc,,
|
||||
deezeridu/settings.py,sha256=ZOURy_oQ-lWQWe41Oyx-ED1wpz9zZBidnQgFpf13XWg,571
|
||||
deezeridu/settings.pyc,,
|
||||
deezeridu/taggers.py,sha256=-iPciR31q9qNrF_QXo3fPmaUCMxoWr8bP_DY3RS9_Zg,4359
|
||||
deezeridu/taggers.pyc,,
|
||||
deezeridu/utils.py,sha256=L9PxpvqbfEUMccFBvy1B1fplsm7cU9uU_TKV5HnxNuM,5432
|
||||
deezeridu/utils.pyc,,
|
@ -1,5 +0,0 @@
|
||||
Wheel-Version: 1.0
|
||||
Generator: bdist_wheel (0.34.2)
|
||||
Root-Is-Purelib: true
|
||||
Tag: py3-none-any
|
||||
|
@ -1,2 +0,0 @@
|
||||
deezeridu
|
||||
deezeridu/models
|
@ -1,301 +0,0 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
from .api import API
|
||||
from .gateway import Gateway
|
||||
from .download import (
|
||||
TrackDownloader, AlbumDownloader, PlaylistDownloader,
|
||||
DownloaderJob
|
||||
)
|
||||
|
||||
from .utils import (
|
||||
create_zip, get_ids, link_is_valid,
|
||||
what_kind, convert_to_date
|
||||
)
|
||||
from .exceptions import (
|
||||
InvalidLink, TrackNotFound,
|
||||
NoDataApi, AlbumNotFound, CredentialsMissing
|
||||
)
|
||||
from .models import (
|
||||
Track, Album, Playlist,
|
||||
Preferences
|
||||
)
|
||||
from json_database import JsonConfigXDG
|
||||
|
||||
|
||||
class Deezer:
|
||||
def __init__(
|
||||
self,
|
||||
arl=None,
|
||||
email=None,
|
||||
password=None
|
||||
):
|
||||
if arl:
|
||||
self.__gw_api = Gateway(arl=arl)
|
||||
else:
|
||||
if not email or not password:
|
||||
creds = JsonConfigXDG("deezer", subfolder="deezeridu")
|
||||
email = creds.get("email")
|
||||
password = creds.get("password")
|
||||
if not email or not password:
|
||||
raise CredentialsMissing
|
||||
self.__gw_api = Gateway(
|
||||
email=email,
|
||||
password=password
|
||||
)
|
||||
|
||||
self.__api = API()
|
||||
self.__download_job = DownloaderJob(self.__api, self.__gw_api)
|
||||
|
||||
def download_track(
|
||||
self, link_track,
|
||||
output_dir,
|
||||
quality_download="MP3_320",
|
||||
recursive_quality=False,
|
||||
recursive_download=False,
|
||||
not_interface=False,
|
||||
method_save=2
|
||||
) -> Track:
|
||||
|
||||
link_is_valid(link_track)
|
||||
ids = get_ids(link_track)
|
||||
|
||||
try:
|
||||
song_metadata = self.__api.tracking(ids)
|
||||
except NoDataApi:
|
||||
infos = self.__gw_api.get_song_data(ids)
|
||||
|
||||
if not "FALLBACK" in infos:
|
||||
raise TrackNotFound(link_track)
|
||||
|
||||
ids = infos['FALLBACK']['SNG_ID']
|
||||
song_metadata = self.__api.tracking(ids)
|
||||
|
||||
preferences = Preferences()
|
||||
|
||||
preferences.link = link_track
|
||||
preferences.song_metadata = song_metadata
|
||||
preferences.quality_download = quality_download
|
||||
preferences.output_dir = output_dir
|
||||
preferences.ids = ids
|
||||
preferences.recursive_quality = recursive_quality
|
||||
preferences.recursive_download = recursive_download
|
||||
preferences.not_interface = not_interface
|
||||
preferences.method_save = method_save
|
||||
|
||||
track = TrackDownloader(preferences, self.__download_job).dw()
|
||||
|
||||
return track
|
||||
|
||||
def download_album(
|
||||
self, link_album,
|
||||
output_dir,
|
||||
quality_download="MP3_320",
|
||||
recursive_quality=False,
|
||||
recursive_download=False,
|
||||
not_interface=False,
|
||||
make_zip=False,
|
||||
method_save=2
|
||||
) -> Album:
|
||||
|
||||
link_is_valid(link_album)
|
||||
ids = get_ids(link_album)
|
||||
|
||||
try:
|
||||
album_json = self.__api.get_album(ids)
|
||||
except NoDataApi:
|
||||
raise AlbumNotFound(link_album)
|
||||
|
||||
song_metadata = {
|
||||
"music": [],
|
||||
"artist": [],
|
||||
"tracknum": [],
|
||||
"discnum": [],
|
||||
"bpm": [],
|
||||
"duration": [],
|
||||
"isrc": [],
|
||||
"gain": [],
|
||||
"album": album_json['title'],
|
||||
"label": album_json['label'],
|
||||
"year": convert_to_date(album_json['release_date']),
|
||||
"upc": album_json['upc'],
|
||||
"nb_tracks": album_json['nb_tracks']
|
||||
}
|
||||
|
||||
genres = []
|
||||
|
||||
if "genres" in album_json:
|
||||
for a in album_json['genres']['data']:
|
||||
genres.append(a['name'])
|
||||
|
||||
song_metadata['genre'] = " & ".join(genres)
|
||||
ar_album = []
|
||||
|
||||
for a in album_json['contributors']:
|
||||
if a['role'] == "Main":
|
||||
ar_album.append(a['name'])
|
||||
|
||||
song_metadata['ar_album'] = " & ".join(ar_album)
|
||||
sm_items = song_metadata.items()
|
||||
|
||||
for track in album_json['tracks']['data']:
|
||||
c_ids = track['id']
|
||||
detas = self.__api.tracking(c_ids, album=True)
|
||||
|
||||
for key, item in sm_items:
|
||||
if type(item) is list:
|
||||
song_metadata[key].append(detas[key])
|
||||
|
||||
preferences = Preferences()
|
||||
|
||||
preferences.link = link_album
|
||||
preferences.song_metadata = song_metadata
|
||||
preferences.quality_download = quality_download
|
||||
preferences.output_dir = output_dir
|
||||
preferences.ids = ids
|
||||
preferences.json_data = album_json
|
||||
preferences.recursive_quality = recursive_quality
|
||||
preferences.recursive_download = recursive_download
|
||||
preferences.not_interface = not_interface
|
||||
preferences.method_save = method_save
|
||||
preferences.make_zip = make_zip
|
||||
|
||||
album = AlbumDownloader(preferences, self.__download_job).dw()
|
||||
|
||||
return album
|
||||
|
||||
def download_playlist(
|
||||
self, link_playlist,
|
||||
output_dir,
|
||||
quality_download="MP3_320",
|
||||
recursive_quality=False,
|
||||
recursive_download=False,
|
||||
not_interface=False,
|
||||
make_zip=False,
|
||||
method_save=2
|
||||
) -> Playlist:
|
||||
|
||||
link_is_valid(link_playlist)
|
||||
ids = get_ids(link_playlist)
|
||||
|
||||
song_metadata = []
|
||||
playlist_json = self.__api.get_playlist(ids)
|
||||
|
||||
for track in playlist_json['tracks']['data']:
|
||||
c_ids = track['id']
|
||||
|
||||
try:
|
||||
c_song_metadata = self.__api.tracking(c_ids)
|
||||
except NoDataApi:
|
||||
infos = self.__gw_api.get_song_data(c_ids)
|
||||
|
||||
if not "FALLBACK" in infos:
|
||||
c_song_metadata = f"{track['title']} - {track['artist']['name']}"
|
||||
else:
|
||||
c_song_metadata = self.__api.tracking(c_ids)
|
||||
|
||||
song_metadata.append(c_song_metadata)
|
||||
|
||||
preferences = Preferences()
|
||||
|
||||
preferences.link = link_playlist
|
||||
preferences.song_metadata = song_metadata
|
||||
preferences.quality_download = quality_download
|
||||
preferences.output_dir = output_dir
|
||||
preferences.ids = ids
|
||||
preferences.json_data = playlist_json
|
||||
preferences.recursive_quality = recursive_quality
|
||||
preferences.recursive_download = recursive_download
|
||||
preferences.not_interface = not_interface
|
||||
preferences.method_save = method_save
|
||||
preferences.make_zip = make_zip
|
||||
|
||||
playlist = PlaylistDownloader(preferences, self.__download_job).dw()
|
||||
|
||||
return playlist
|
||||
|
||||
def download_artist_toptracks(
|
||||
self, link_artist,
|
||||
output_dir,
|
||||
quality_download="MP3_320",
|
||||
recursive_quality=False,
|
||||
recursive_download=False,
|
||||
not_interface=False
|
||||
):
|
||||
|
||||
link_is_valid(link_artist)
|
||||
ids = get_ids(link_artist)
|
||||
|
||||
playlist_json = self.__api.get_artist_top_tracks(ids)['data']
|
||||
|
||||
names = [
|
||||
self.download_track(
|
||||
track['link'], output_dir,
|
||||
quality_download, recursive_quality,
|
||||
recursive_download, not_interface
|
||||
)
|
||||
|
||||
for track in playlist_json
|
||||
]
|
||||
return Playlist(names)
|
||||
|
||||
def download(
|
||||
self, link,
|
||||
output_dir,
|
||||
quality_download="MP3_320",
|
||||
recursive_quality=False,
|
||||
recursive_download=False,
|
||||
not_interface=False,
|
||||
make_zip=False,
|
||||
method_save=2
|
||||
):
|
||||
|
||||
link_is_valid(link)
|
||||
link = what_kind(link)
|
||||
|
||||
if "first_result/" in link or "track/" in link:
|
||||
return self.download_track(
|
||||
link,
|
||||
output_dir=output_dir,
|
||||
quality_download=quality_download,
|
||||
recursive_quality=recursive_quality,
|
||||
recursive_download=recursive_download,
|
||||
not_interface=not_interface,
|
||||
method_save=2
|
||||
)
|
||||
elif "album/" in link:
|
||||
return self.download_album(
|
||||
link,
|
||||
output_dir=output_dir,
|
||||
quality_download=quality_download,
|
||||
recursive_quality=recursive_quality,
|
||||
recursive_download=recursive_download,
|
||||
not_interface=not_interface,
|
||||
make_zip=make_zip,
|
||||
method_save=2
|
||||
)
|
||||
elif "artist/" in link:
|
||||
return self.download_artist_toptracks(
|
||||
link,
|
||||
output_dir=output_dir,
|
||||
quality_download=quality_download,
|
||||
recursive_quality=recursive_quality,
|
||||
recursive_download=recursive_download,
|
||||
not_interface=not_interface
|
||||
)
|
||||
|
||||
elif "playlist/" in link:
|
||||
return self.download_playlist(
|
||||
link,
|
||||
output_dir=output_dir,
|
||||
quality_download=quality_download,
|
||||
recursive_quality=recursive_quality,
|
||||
recursive_download=recursive_download,
|
||||
not_interface=not_interface,
|
||||
make_zip=make_zip,
|
||||
method_save=2
|
||||
)
|
||||
|
||||
smart.type = "playlist"
|
||||
smart._playlist = playlist
|
||||
|
||||
raise InvalidLink(link)
|
Binary file not shown.
@ -1,212 +0,0 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
from time import sleep
|
||||
|
||||
from requests import get as req_get
|
||||
|
||||
from .settings import header
|
||||
from .utils import artist_sort, convert_to_date
|
||||
from .exceptions import (
|
||||
NoDataApi, QuotaExceeded, TrackNotFound
|
||||
)
|
||||
|
||||
|
||||
class API:
|
||||
def __init__(self):
|
||||
self.__api_link = "https://api.deezer.com/"
|
||||
self.__cover = "https://e-cdns-images.dzcdn.net/images/cover/%s/{}-000000-80-0-0.jpg"
|
||||
|
||||
def __get_api(self, url, quota_exceeded=False):
|
||||
json = req_get(url, headers=header).json()
|
||||
|
||||
if "error" in json:
|
||||
if json['error']['message'] == "no data":
|
||||
raise NoDataApi("No data avalaible :(")
|
||||
|
||||
elif json['error']['message'] == "Quota limit exceeded":
|
||||
if not quota_exceeded:
|
||||
sleep(0.8)
|
||||
json = self.__get_api(url, True)
|
||||
else:
|
||||
raise QuotaExceeded
|
||||
|
||||
return json
|
||||
|
||||
def get_chart(self, index=0):
|
||||
url = f"{self.__api_link}chart/{index}"
|
||||
infos = self.__get_api(url)
|
||||
return infos
|
||||
|
||||
def get_track(self, ids):
|
||||
url = f"{self.__api_link}track/{ids}"
|
||||
infos = self.__get_api(url)
|
||||
return infos
|
||||
|
||||
def get_album(self, ids):
|
||||
url = f"{self.__api_link}album/{ids}"
|
||||
infos = self.__get_api(url)
|
||||
return infos
|
||||
|
||||
def get_playlist(self, ids):
|
||||
url = f"{self.__api_link}playlist/{ids}"
|
||||
infos = self.__get_api(url)
|
||||
return infos
|
||||
|
||||
def get_artist(self, ids):
|
||||
url = f"{self.__api_link}artist/{ids}"
|
||||
infos = self.__get_api(url)
|
||||
return infos
|
||||
|
||||
def get_artist_top_tracks(self, ids, limit=25):
|
||||
url = f"{self.__api_link}artist/{ids}/top?limit={limit}"
|
||||
infos = self.__get_api(url)
|
||||
return infos
|
||||
|
||||
def get_artist_top_albums(self, ids, limit=25):
|
||||
url = f"{self.__api_link}artist/{ids}/albums?limit={limit}"
|
||||
infos = self.__get_api(url)
|
||||
return infos
|
||||
|
||||
def get_artist_related(self, ids):
|
||||
url = f"{self.__api_link}artist/{ids}/related"
|
||||
infos = self.__get_api(url)
|
||||
return infos
|
||||
|
||||
def get_artist_radio(self, ids):
|
||||
url = f"{self.__api_link}artist/{ids}/radio"
|
||||
infos = self.__get_api(url)
|
||||
return infos
|
||||
|
||||
def get_artist_top_playlists(self, ids, limit=25):
|
||||
url = f"{self.__api_link}artist/{ids}/playlists?limit={limit}"
|
||||
infos = self.__get_api(url)
|
||||
return infos
|
||||
|
||||
def search(self, query):
|
||||
url = f"{self.__api_link}search/?q={query}"
|
||||
infos = self.__get_api(url)
|
||||
|
||||
if infos['total'] == 0:
|
||||
raise NoDataApi(query)
|
||||
|
||||
return infos
|
||||
|
||||
def search_track(self, query):
|
||||
url = f"{self.__api_link}search/track/?q={query}"
|
||||
infos = self.__get_api(url)
|
||||
|
||||
if infos['total'] == 0:
|
||||
raise NoDataApi(query)
|
||||
|
||||
return infos
|
||||
|
||||
def search_album(self, query):
|
||||
url = f"{self.__api_link}search/album/?q={query}"
|
||||
infos = self.__get_api(url)
|
||||
|
||||
if infos['total'] == 0:
|
||||
raise NoDataApi(query)
|
||||
|
||||
return infos
|
||||
|
||||
def search_playlist(self, query):
|
||||
url = f"{self.__api_link}search/playlist/?q={query}"
|
||||
infos = self.__get_api(url)
|
||||
|
||||
if infos['total'] == 0:
|
||||
raise NoDataApi(query)
|
||||
|
||||
return infos
|
||||
|
||||
def search_artist(self, query):
|
||||
url = f"{self.__api_link}search/artist/?q={query}"
|
||||
infos = self.__get_api(url)
|
||||
|
||||
if infos['total'] == 0:
|
||||
raise NoDataApi(query)
|
||||
|
||||
return infos
|
||||
|
||||
def not_found(self, song, title):
|
||||
try:
|
||||
data = self.search_track(song)['data']
|
||||
except NoDataApi:
|
||||
raise TrackNotFound(song)
|
||||
|
||||
ids = None
|
||||
|
||||
for track in data:
|
||||
if (
|
||||
track['title'] == title
|
||||
) or (
|
||||
title in track['title_short']
|
||||
):
|
||||
ids = track['id']
|
||||
break
|
||||
|
||||
if not ids:
|
||||
raise TrackNotFound(song)
|
||||
|
||||
return str(ids)
|
||||
|
||||
def get_img_url(self, md5_image, size="1200x1200"):
|
||||
cover = self.__cover.format(size)
|
||||
image_url = cover % md5_image
|
||||
return image_url
|
||||
|
||||
def choose_img(self, md5_image, size="1200x1200"):
|
||||
image_url = self.get_img_url(md5_image, size)
|
||||
image = req_get(image_url).content
|
||||
|
||||
if len(image) == 13:
|
||||
image_url = self.get_img_url("", size)
|
||||
image = req_get(image_url).content
|
||||
|
||||
return image
|
||||
|
||||
def tracking(self, ids, album=False):
|
||||
datas = {}
|
||||
json_track = self.get_track(ids)
|
||||
|
||||
if not album:
|
||||
album_ids = json_track['album']['id']
|
||||
json_album = self.get_album(album_ids)
|
||||
genres = []
|
||||
|
||||
if "genres" in json_album:
|
||||
for genre in json_album['genres']['data']:
|
||||
genres.append(genre['name'])
|
||||
|
||||
datas['genre'] = " & ".join(genres)
|
||||
ar_album = []
|
||||
|
||||
for contributor in json_album['contributors']:
|
||||
if contributor['role'] == "Main":
|
||||
ar_album.append(contributor['name'])
|
||||
|
||||
datas['ar_album'] = " & ".join(ar_album)
|
||||
datas['album'] = json_album['title']
|
||||
datas['label'] = json_album['label']
|
||||
datas['upc'] = json_album['upc']
|
||||
datas['nb_tracks'] = json_album['nb_tracks']
|
||||
|
||||
datas['music'] = json_track['title']
|
||||
array = []
|
||||
|
||||
for contributor in json_track['contributors']:
|
||||
if contributor['name'] != "":
|
||||
array.append(contributor['name'])
|
||||
|
||||
array.append(
|
||||
json_track['artist']['name']
|
||||
)
|
||||
|
||||
datas['artist'] = artist_sort(array)
|
||||
datas['tracknum'] = json_track['track_position']
|
||||
datas['discnum'] = json_track['disk_number']
|
||||
datas['year'] = convert_to_date(json_track['release_date'])
|
||||
datas['bpm'] = json_track['bpm']
|
||||
datas['duration'] = json_track['duration']
|
||||
datas['isrc'] = json_track['isrc']
|
||||
datas['gain'] = json_track['gain']
|
||||
return datas
|
Binary file not shown.
@ -1,519 +0,0 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
from copy import deepcopy
|
||||
from os.path import isfile
|
||||
|
||||
from tqdm import tqdm
|
||||
|
||||
from .api import API
|
||||
from .gateway import Gateway
|
||||
from .settings import qualities
|
||||
from .download_utils import decryptfile, gen_song_hash
|
||||
from .taggers import write_tags, check_track
|
||||
from .utils import (
|
||||
set_path, trasform_sync_lyric,
|
||||
create_zip, check_track_ids,
|
||||
check_track_md5, check_track_token
|
||||
)
|
||||
from .exceptions import (
|
||||
TrackNotFound, NoRightOnMedia, QualityNotFound
|
||||
)
|
||||
from .models import (
|
||||
Track, Album, Playlist,
|
||||
Preferences,
|
||||
)
|
||||
|
||||
|
||||
class DownloaderJob:
|
||||
def __init__(
|
||||
self,
|
||||
api: API,
|
||||
gw_api: Gateway
|
||||
) -> None:
|
||||
|
||||
self.api = api
|
||||
self.gw_api = gw_api
|
||||
|
||||
def __get_url(
|
||||
self,
|
||||
c_track: Track,
|
||||
quality_download: str
|
||||
) -> dict:
|
||||
|
||||
c_md5, c_media_version = check_track_md5(c_track)
|
||||
c_ids = check_track_ids(c_track)
|
||||
n_quality = qualities[quality_download]['n_quality']
|
||||
|
||||
c_song_hash = gen_song_hash(
|
||||
c_md5, n_quality,
|
||||
c_ids, c_media_version
|
||||
)
|
||||
|
||||
c_media_url = self.gw_api.get_song_url(c_md5[0], c_song_hash)
|
||||
|
||||
c_media_json = {
|
||||
"media": [
|
||||
{
|
||||
"sources": [
|
||||
{
|
||||
"url": c_media_url
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
return c_media_json
|
||||
|
||||
def check_sources(
|
||||
self,
|
||||
infos_dw: list,
|
||||
quality_download: str
|
||||
) -> list:
|
||||
|
||||
tracks_token = [
|
||||
check_track_token(c_track)
|
||||
for c_track in infos_dw
|
||||
]
|
||||
|
||||
try:
|
||||
medias = self.gw_api.get_medias_url(tracks_token, quality_download)
|
||||
|
||||
for a in range(
|
||||
len(medias)
|
||||
):
|
||||
if "errors" in medias[a]:
|
||||
c_media_json = self.__get_url(infos_dw[a],
|
||||
quality_download)
|
||||
medias[a] = c_media_json
|
||||
else:
|
||||
if not medias[a]['media']:
|
||||
c_media_json = self.__get_url(infos_dw[a],
|
||||
quality_download)
|
||||
medias[a] = c_media_json
|
||||
except NoRightOnMedia:
|
||||
medias = []
|
||||
|
||||
for c_track in infos_dw:
|
||||
c_media_json = self.__get_url(c_track, quality_download)
|
||||
medias.append(c_media_json)
|
||||
|
||||
return medias
|
||||
|
||||
|
||||
class Downloader:
|
||||
def __init__(
|
||||
self,
|
||||
infos_dw: dict,
|
||||
preferences: Preferences,
|
||||
download_job: DownloaderJob,
|
||||
) -> None:
|
||||
|
||||
self.__download_job = download_job
|
||||
self.__api = download_job.api
|
||||
self.__gw_api = download_job.gw_api
|
||||
|
||||
self.__infos_dw = infos_dw
|
||||
|
||||
self.__ids = preferences.ids
|
||||
self.__link = preferences.link
|
||||
self.__output_dir = preferences.output_dir
|
||||
self.__method_save = preferences.method_save
|
||||
self.__song_metadata = preferences.song_metadata
|
||||
self.__not_interface = preferences.not_interface
|
||||
self.__quality_download = preferences.quality_download
|
||||
self.__recursive_quality = preferences.recursive_quality
|
||||
self.__recursive_download = preferences.recursive_download
|
||||
|
||||
self.__c_quality = qualities[self.__quality_download]
|
||||
self.__set_quality()
|
||||
self.__set_song_path()
|
||||
|
||||
def __set_quality(self) -> None:
|
||||
self.__file_format = self.__c_quality['f_format']
|
||||
self.__song_quality = self.__c_quality['s_quality']
|
||||
|
||||
def __set_song_path(self) -> None:
|
||||
self.__song_path = set_path(
|
||||
self.__song_metadata,
|
||||
self.__output_dir,
|
||||
self.__song_quality,
|
||||
self.__file_format,
|
||||
self.__method_save
|
||||
)
|
||||
|
||||
def __write_track(self) -> None:
|
||||
self.__set_song_path()
|
||||
|
||||
self.__c_track = Track(
|
||||
self.__song_metadata, self.__song_path,
|
||||
self.__file_format, self.__song_quality,
|
||||
self.__link, self.__ids
|
||||
)
|
||||
|
||||
def easy_dw(self) -> Track:
|
||||
pic = self.__infos_dw['ALB_PICTURE']
|
||||
image = self.__api.choose_img(pic)
|
||||
self.__song_metadata['image'] = image
|
||||
song = f"{self.__song_metadata['music']} - {self.__song_metadata['artist']}"
|
||||
|
||||
if not self.__not_interface:
|
||||
print(f"Downloading: {song}")
|
||||
|
||||
try:
|
||||
self.download_try()
|
||||
except TrackNotFound:
|
||||
try:
|
||||
ids = self.__api.not_found(song, self.__song_metadata['music'])
|
||||
self.__infos_dw = self.__gw_api.get_song_data(ids)
|
||||
|
||||
media = self.__download_job.check_sources(
|
||||
[self.__infos_dw], self.__quality_download
|
||||
)
|
||||
|
||||
self.__infos_dw['media_url'] = media[0]
|
||||
self.download_try()
|
||||
except TrackNotFound:
|
||||
self.__c_track = Track(
|
||||
self.__song_metadata,
|
||||
None, None,
|
||||
None, None, None,
|
||||
)
|
||||
|
||||
self.__c_track.success = False
|
||||
|
||||
self.__c_track.md5_image = pic
|
||||
|
||||
return self.__c_track
|
||||
|
||||
def download_try(self) -> Track:
|
||||
self.__c_track = Track(
|
||||
self.__song_metadata, self.__song_path,
|
||||
self.__file_format, self.__song_quality,
|
||||
self.__link, self.__ids
|
||||
)
|
||||
|
||||
if isfile(self.__song_path):
|
||||
if check_track(self.__c_track):
|
||||
return self.__c_track
|
||||
|
||||
media_list = self.__infos_dw['media_url']['media']
|
||||
song_link = media_list[0]['sources'][0]['url']
|
||||
|
||||
try:
|
||||
crypted_audio = self.__gw_api.song_exist(song_link)
|
||||
except TrackNotFound:
|
||||
song = self.__song_metadata['music']
|
||||
artist = self.__song_metadata['artist']
|
||||
msg = f"\n⚠ The {song} - {artist} can't be downloaded in {self.__quality_download} quality :( ⚠\n"
|
||||
|
||||
if not self.__recursive_quality:
|
||||
raise QualityNotFound(msg=msg)
|
||||
|
||||
print(msg)
|
||||
|
||||
for c_quality in qualities:
|
||||
if self.__quality_download == c_quality:
|
||||
continue
|
||||
|
||||
print(
|
||||
f"🛈 Trying to download {song} - {artist} in {c_quality}")
|
||||
|
||||
media = self.__download_job.check_sources(
|
||||
[self.__infos_dw], c_quality
|
||||
)
|
||||
|
||||
self.__infos_dw['media_url'] = media[0]
|
||||
c_media = self.__infos_dw['media_url']
|
||||
media_list = c_media['media']
|
||||
song_link = media_list[0]['sources'][0]['url']
|
||||
|
||||
try:
|
||||
crypted_audio = self.__gw_api.song_exist(song_link)
|
||||
self.__c_quality = qualities[c_quality]
|
||||
self.__set_quality()
|
||||
break
|
||||
except TrackNotFound:
|
||||
if c_quality == "MP3_128":
|
||||
raise TrackNotFound("Error with this song",
|
||||
self.__link)
|
||||
|
||||
self.__write_track()
|
||||
c_crypted_audio = crypted_audio.iter_content(2048)
|
||||
c_ids = check_track_ids(self.__infos_dw)
|
||||
self.__c_track.set_fallback_ids(c_ids)
|
||||
|
||||
decryptfile(
|
||||
c_crypted_audio, c_ids, self.__song_path
|
||||
)
|
||||
|
||||
self.__add_more_tags()
|
||||
write_tags(self.__c_track)
|
||||
|
||||
return self.__c_track
|
||||
|
||||
def __add_more_tags(self) -> None:
|
||||
contributors = self.__infos_dw['SNG_CONTRIBUTORS']
|
||||
|
||||
if "author" in contributors:
|
||||
self.__song_metadata['author'] = " & ".join(
|
||||
contributors['author']
|
||||
)
|
||||
else:
|
||||
self.__song_metadata['author'] = ""
|
||||
|
||||
if "composer" in contributors:
|
||||
self.__song_metadata['composer'] = " & ".join(
|
||||
contributors['composer']
|
||||
)
|
||||
else:
|
||||
self.__song_metadata['composer'] = ""
|
||||
|
||||
if "lyricist" in contributors:
|
||||
self.__song_metadata['lyricist'] = " & ".join(
|
||||
contributors['lyricist']
|
||||
)
|
||||
else:
|
||||
self.__song_metadata['lyricist'] = ""
|
||||
|
||||
if "composerlyricist" in contributors:
|
||||
self.__song_metadata['composer'] = " & ".join(
|
||||
contributors['composerlyricist']
|
||||
)
|
||||
else:
|
||||
self.__song_metadata['composerlyricist'] = ""
|
||||
|
||||
if "version" in self.__infos_dw:
|
||||
self.__song_metadata['version'] = self.__infos_dw['VERSION']
|
||||
else:
|
||||
self.__song_metadata['version'] = ""
|
||||
|
||||
self.__song_metadata['lyric'] = ""
|
||||
self.__song_metadata['copyright'] = ""
|
||||
self.__song_metadata['lyricist'] = ""
|
||||
self.__song_metadata['lyric_sync'] = []
|
||||
|
||||
if self.__infos_dw['LYRICS_ID'] != 0:
|
||||
need = self.__gw_api.get_lyric(self.__ids)
|
||||
|
||||
if "LYRICS_SYNC_JSON" in need:
|
||||
self.__song_metadata['lyric_sync'] = trasform_sync_lyric(
|
||||
need['LYRICS_SYNC_JSON']
|
||||
)
|
||||
|
||||
self.__song_metadata['lyric'] = need['LYRICS_TEXT']
|
||||
self.__song_metadata['copyright'] = need['LYRICS_COPYRIGHTS']
|
||||
self.__song_metadata['lyricist'] = need['LYRICS_WRITERS']
|
||||
|
||||
|
||||
class TrackDownloader:
|
||||
def __init__(
|
||||
self,
|
||||
preferences: Preferences,
|
||||
download_job: DownloaderJob
|
||||
) -> None:
|
||||
self.__download_job = download_job
|
||||
self.__gw_api = download_job.gw_api
|
||||
|
||||
self.__preferences = preferences
|
||||
self.__ids = self.__preferences.ids
|
||||
self.__song_metadata = self.__preferences.song_metadata
|
||||
self.__quality_download = self.__preferences.quality_download
|
||||
|
||||
def dw(self) -> Track:
|
||||
infos_dw = self.__gw_api.get_song_data(self.__ids)
|
||||
|
||||
media = self.__download_job.check_sources(
|
||||
[infos_dw], self.__quality_download
|
||||
)
|
||||
|
||||
infos_dw['media_url'] = media[0]
|
||||
|
||||
track = Downloader(
|
||||
infos_dw, self.__preferences, self.__download_job,
|
||||
).easy_dw()
|
||||
|
||||
if not track.success:
|
||||
song = f"{self.__song_metadata['music']} - {self.__song_metadata['artist']}"
|
||||
error_msg = f"Cannot download {song}"
|
||||
|
||||
raise TrackNotFound(message=error_msg)
|
||||
|
||||
return track
|
||||
|
||||
|
||||
class AlbumDownloader:
|
||||
def __init__(
|
||||
self,
|
||||
preferences: Preferences,
|
||||
download_job: DownloaderJob
|
||||
) -> None:
|
||||
|
||||
self.__api = download_job.api
|
||||
self.__download_job = download_job
|
||||
self.__gw_api = download_job.gw_api
|
||||
|
||||
self.__preferences = preferences
|
||||
self.__ids = self.__preferences.ids
|
||||
self.__make_zip = self.__preferences.make_zip
|
||||
self.__output_dir = self.__preferences.output_dir
|
||||
self.__method_save = self.__preferences.method_save
|
||||
self.__song_metadata = self.__preferences.song_metadata
|
||||
self.__not_interface = self.__preferences.not_interface
|
||||
self.__quality_download = self.__preferences.quality_download
|
||||
|
||||
self.__song_metadata_items = self.__song_metadata.items()
|
||||
|
||||
def dw(self) -> Album:
|
||||
infos_dw = self.__gw_api.get_album_data(self.__ids)['data']
|
||||
md5_image = infos_dw[0]['ALB_PICTURE']
|
||||
image = self.__api.choose_img(md5_image)
|
||||
self.__song_metadata['image'] = image
|
||||
|
||||
album = Album(self.__ids)
|
||||
album.image = image
|
||||
album.md5_image = md5_image
|
||||
album.nb_tracks = self.__song_metadata['nb_tracks']
|
||||
album.album_name = self.__song_metadata['album']
|
||||
album.upc = self.__song_metadata['upc']
|
||||
tracks = album.tracks
|
||||
|
||||
medias = self.__download_job.check_sources(
|
||||
infos_dw, self.__quality_download
|
||||
)
|
||||
|
||||
c_song_metadata = {}
|
||||
|
||||
for key, item in self.__song_metadata_items:
|
||||
if type(item) is not list:
|
||||
c_song_metadata[key] = self.__song_metadata[key]
|
||||
|
||||
t = tqdm(
|
||||
range(
|
||||
len(infos_dw)
|
||||
),
|
||||
desc=c_song_metadata['album'],
|
||||
disable=self.__not_interface
|
||||
)
|
||||
|
||||
for a in t:
|
||||
for key, item in self.__song_metadata_items:
|
||||
if type(item) is list:
|
||||
c_song_metadata[key] = self.__song_metadata[key][a]
|
||||
|
||||
c_infos_dw = infos_dw[a]
|
||||
c_infos_dw['media_url'] = medias[a]
|
||||
song = f"{c_song_metadata['music']} - {c_song_metadata['artist']}"
|
||||
t.set_description_str(song)
|
||||
c_preferences = deepcopy(self.__preferences)
|
||||
c_preferences.song_metadata = c_song_metadata
|
||||
c_preferences.ids = c_infos_dw['SNG_ID']
|
||||
|
||||
try:
|
||||
track = Downloader(
|
||||
c_infos_dw, c_preferences, self.__download_job
|
||||
).download_try()
|
||||
|
||||
tracks.append(track)
|
||||
except TrackNotFound:
|
||||
try:
|
||||
ids = self.__api.not_found(song, c_song_metadata['music'])
|
||||
c_song_data = self.__gw_api.get_song_data(ids)
|
||||
|
||||
c_media = self.__download_job.check_sources(
|
||||
[c_song_data], self.__quality_download
|
||||
)
|
||||
|
||||
c_infos_dw['media_url'] = c_media[0]
|
||||
|
||||
track = Downloader(
|
||||
c_infos_dw, c_preferences, self.__download_job
|
||||
).download_try()
|
||||
|
||||
tracks.append(track)
|
||||
except TrackNotFound:
|
||||
track = Track(
|
||||
c_song_metadata,
|
||||
None, None,
|
||||
None, None, None,
|
||||
)
|
||||
|
||||
track.success = False
|
||||
tracks.append(track)
|
||||
print(f"Track not found: {song} :(")
|
||||
continue
|
||||
|
||||
if self.__make_zip:
|
||||
song_quality = tracks[0].quality
|
||||
|
||||
zip_name = create_zip(
|
||||
tracks,
|
||||
output_dir=self.__output_dir,
|
||||
song_metadata=self.__song_metadata,
|
||||
song_quality=song_quality,
|
||||
method_save=self.__method_save
|
||||
)
|
||||
|
||||
album.zip_path = zip_name
|
||||
|
||||
return album
|
||||
|
||||
|
||||
class PlaylistDownloader:
|
||||
def __init__(
|
||||
self,
|
||||
preferences: Preferences,
|
||||
download_job: DownloaderJob
|
||||
) -> None:
|
||||
|
||||
self.__download_job = download_job
|
||||
self.__gw_api = download_job.gw_api
|
||||
|
||||
self.__preferences = preferences
|
||||
self.__ids = self.__preferences.ids
|
||||
self.__json_data = preferences.json_data
|
||||
self.__make_zip = self.__preferences.make_zip
|
||||
self.__output_dir = self.__preferences.output_dir
|
||||
self.__song_metadata = self.__preferences.song_metadata
|
||||
self.__quality_download = self.__preferences.quality_download
|
||||
|
||||
def dw(self) -> Playlist:
|
||||
infos_dw = self.__gw_api.get_playlist_data(self.__ids)['data']
|
||||
|
||||
playlist = Playlist()
|
||||
tracks = playlist.tracks
|
||||
|
||||
medias = self.__download_job.check_sources(
|
||||
infos_dw, self.__quality_download
|
||||
)
|
||||
|
||||
for c_infos_dw, c_media, c_song_metadata in zip(
|
||||
infos_dw, medias, self.__song_metadata
|
||||
):
|
||||
c_infos_dw['media_url'] = c_media
|
||||
c_preferences = deepcopy(self.__preferences)
|
||||
c_preferences.ids = c_infos_dw['SNG_ID']
|
||||
c_preferences.song_metadata = c_song_metadata
|
||||
c_song_metadata = c_preferences.song_metadata
|
||||
|
||||
if type(c_song_metadata) is str:
|
||||
print(f"Track not found {c_song_metadata} :(")
|
||||
continue
|
||||
|
||||
track = Downloader(
|
||||
c_infos_dw, c_preferences, self.__download_job
|
||||
).easy_dw()
|
||||
|
||||
if not track.success:
|
||||
song = f"{c_song_metadata['music']} - {c_song_metadata['artist']}"
|
||||
print(f"Cannot download {song}")
|
||||
|
||||
tracks.append(track)
|
||||
|
||||
if self.__make_zip:
|
||||
playlist_title = self.__json_data['title']
|
||||
zip_name = f"{self.__output_dir}/{playlist_title} [playlist {self.__ids}]"
|
||||
create_zip(tracks, zip_name=zip_name)
|
||||
playlist.zip_path = zip_name
|
||||
|
||||
return playlist
|
Binary file not shown.
@ -1,99 +0,0 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
from binascii import (
|
||||
a2b_hex as __a2b_hex,
|
||||
b2a_hex as __b2a_hex
|
||||
)
|
||||
from hashlib import md5 as __md5
|
||||
|
||||
from Cryptodome.Cipher.AES import (
|
||||
new as __newAES,
|
||||
MODE_ECB as __MODE_ECB
|
||||
)
|
||||
from Cryptodome.Cipher.Blowfish import (
|
||||
new as __newBlowfish,
|
||||
MODE_CBC as __MODE_CBC
|
||||
)
|
||||
|
||||
__secret_key = "g4el58wc0zvf9na1"
|
||||
__secret_key2 = b"jo6aey6haid2Teih"
|
||||
__idk = __a2b_hex("0001020304050607")
|
||||
|
||||
|
||||
def md5hex(data: str):
|
||||
hashed = __md5(
|
||||
data.encode()
|
||||
).hexdigest()
|
||||
|
||||
return hashed
|
||||
|
||||
|
||||
def gen_song_hash(md5, quality, ids, media):
|
||||
data = b"\xa4".join(
|
||||
a.encode()
|
||||
for a in [
|
||||
md5, quality, ids, media
|
||||
]
|
||||
)
|
||||
|
||||
hashed = (
|
||||
__md5(data)
|
||||
.hexdigest()
|
||||
.encode()
|
||||
)
|
||||
|
||||
data = b"\xa4".join(
|
||||
[hashed, data]
|
||||
) + b"\xa4"
|
||||
|
||||
if len(data) % 16:
|
||||
data += b"\x00" * (16 - len(data) % 16)
|
||||
|
||||
c = __newAES(__secret_key2, __MODE_ECB)
|
||||
|
||||
media_url = __b2a_hex(
|
||||
c.encrypt(data)
|
||||
).decode()
|
||||
|
||||
return media_url
|
||||
|
||||
|
||||
def __calcbfkey(songid):
|
||||
h = md5hex(songid)
|
||||
|
||||
bfkey = "".join(
|
||||
chr(
|
||||
ord(h[i]) ^ ord(h[i + 16]) ^ ord(__secret_key[i])
|
||||
)
|
||||
|
||||
for i in range(16)
|
||||
)
|
||||
|
||||
return bfkey
|
||||
|
||||
|
||||
def __blowfishDecrypt(data, key):
|
||||
c = __newBlowfish(
|
||||
key.encode(), __MODE_CBC, __idk
|
||||
)
|
||||
|
||||
return c.decrypt(data)
|
||||
|
||||
|
||||
def decryptfile(content, key, name):
|
||||
key = __calcbfkey(key)
|
||||
decrypted_audio = open(name, "wb")
|
||||
seg = 0
|
||||
|
||||
for data in content:
|
||||
if (
|
||||
(seg % 3) == 0
|
||||
) and (
|
||||
len(data) == 2048
|
||||
):
|
||||
data = __blowfishDecrypt(data, key)
|
||||
|
||||
decrypted_audio.write(data)
|
||||
seg += 1
|
||||
|
||||
decrypted_audio.close()
|
Binary file not shown.
@ -1,87 +0,0 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
class TrackNotFound(Exception):
|
||||
def __init__(self, url=None, message=None):
|
||||
self.url = url
|
||||
|
||||
if not message:
|
||||
self.message = f"Track {self.url} not found :("
|
||||
else:
|
||||
self.message = message
|
||||
|
||||
super().__init__(self.message)
|
||||
|
||||
|
||||
class AlbumNotFound(Exception):
|
||||
def __init__(self, url=None):
|
||||
self.url = url
|
||||
self.msg = f"Album {self.url} not found :("
|
||||
super().__init__(self.msg)
|
||||
|
||||
|
||||
class InvalidLink(Exception):
|
||||
def __init__(self, url):
|
||||
self.url = url
|
||||
self.msg = f"Invalid Link {self.url} :("
|
||||
super().__init__(self.msg)
|
||||
|
||||
|
||||
class QuotaExceeded(Exception):
|
||||
def __init__(self, message=None):
|
||||
if not message:
|
||||
self.message = "TOO MUCH REQUESTS LIMIT YOURSELF !!! :)"
|
||||
|
||||
super().__init__(self.message)
|
||||
|
||||
|
||||
class QualityNotFound(Exception):
|
||||
def __init__(self, quality=None, msg=None):
|
||||
self.quality = quality
|
||||
|
||||
if not msg:
|
||||
self.msg = (
|
||||
f"The {quality} quality doesn't exist :)\
|
||||
\nThe qualities have to be FLAC or MP3_320 or MP3_256 or MP3_128"
|
||||
)
|
||||
else:
|
||||
self.msg = msg
|
||||
|
||||
super().__init__(self.msg)
|
||||
|
||||
|
||||
class NoRightOnMedia(Exception):
|
||||
def __init__(self, msg):
|
||||
self.msg = msg
|
||||
super().__init__(msg)
|
||||
|
||||
|
||||
class NoDataApi(Exception):
|
||||
def __init__(self, message):
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
class BadCredentials(Exception):
|
||||
def __init__(
|
||||
self,
|
||||
arl=None,
|
||||
email=None,
|
||||
password=None,
|
||||
msg=None
|
||||
):
|
||||
if msg:
|
||||
self.msg = msg
|
||||
else:
|
||||
self.arl = arl
|
||||
self.email = email
|
||||
self.password = password
|
||||
|
||||
if arl:
|
||||
self.msg = f"Wrong token: {arl} :("
|
||||
else:
|
||||
self.msg = f"Wrong credentials email: {self.email}, password: {self.password}"
|
||||
|
||||
super().__init__(self.msg)
|
||||
|
||||
|
||||
class CredentialsMissing(Exception):
|
||||
""" Deezer credentials not set! """
|
Binary file not shown.
@ -1,279 +0,0 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
from requests import Session
|
||||
from requests import (
|
||||
get as req_get,
|
||||
post as req_post
|
||||
)
|
||||
|
||||
from .settings import qualities
|
||||
from .download_utils import md5hex
|
||||
from .exceptions import (
|
||||
BadCredentials, TrackNotFound, NoRightOnMedia
|
||||
)
|
||||
|
||||
client_id = "172365"
|
||||
client_secret = "fb0bec7ccc063dab0417eb7b0d847f34"
|
||||
try_link = "https://api.deezer.com/platform/generic/track/3135556"
|
||||
|
||||
|
||||
class Gateway:
|
||||
def __init__(
|
||||
self,
|
||||
arl=None,
|
||||
email=None,
|
||||
password=None
|
||||
):
|
||||
self.__req = Session()
|
||||
self.__arl = arl
|
||||
self.__email = email
|
||||
self.__password = password
|
||||
self.__token = "null"
|
||||
self.__get_lyric = "song.getLyrics"
|
||||
self.__get_song_data = "song.getData"
|
||||
self.__get_user_getArl = "user.getArl"
|
||||
self.__get_page_track = "deezer.pageTrack"
|
||||
self.__get_user_data = "deezer.getUserData"
|
||||
self.__get_album_data = "song.getListByAlbum"
|
||||
self.__get_playlist_data = "playlist.getSongs"
|
||||
self.__get_media_url = "https://media.deezer.com/v1/get_url"
|
||||
self.__get_auth_token_url = "https://api.deezer.com/auth/token"
|
||||
self.__private_api_link = "https://www.deezer.com/ajax/gw-light.php"
|
||||
self.__song_server = "https://e-cdns-proxy-{}.dzcdn.net/mobile/1/{}"
|
||||
self.__refresh_token()
|
||||
|
||||
def __login(self):
|
||||
if (
|
||||
(not self.__arl) and
|
||||
(not self.__email) and
|
||||
(not self.__password)
|
||||
):
|
||||
msg = f"NO LOGIN STUFF INSERTED :)))"
|
||||
|
||||
raise BadCredentials(msg=msg)
|
||||
|
||||
if self.__arl:
|
||||
self.__req.cookies['arl'] = self.__arl
|
||||
else:
|
||||
self.__get_arl()
|
||||
|
||||
def __get_arl(self):
|
||||
access_token = self.__get_access_token()
|
||||
|
||||
c_headers = {
|
||||
"Authorization": f"Bearer {access_token}"
|
||||
}
|
||||
|
||||
self.__req.get(try_link, headers=c_headers).json()
|
||||
arl = self.__get_api(self.__get_user_getArl)
|
||||
self.__req.cookies.get("sid")
|
||||
self.__arl = arl
|
||||
|
||||
def __get_access_token(self):
|
||||
password = md5hex(self.__password)
|
||||
|
||||
request_hash = md5hex(
|
||||
"".join(
|
||||
[
|
||||
client_id, self.__email, password, client_secret
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
params = {
|
||||
"app_id": client_id,
|
||||
"login": self.__email,
|
||||
"password": password,
|
||||
"hash": request_hash
|
||||
}
|
||||
|
||||
results = req_get(self.__get_auth_token_url, params=params).json()
|
||||
|
||||
if "error" in results:
|
||||
raise BadCredentials(
|
||||
email=self.__email,
|
||||
password=self.__password
|
||||
)
|
||||
|
||||
access_token = results['access_token']
|
||||
|
||||
return access_token
|
||||
|
||||
def __cool_api(self):
|
||||
guest_sid = self.__req.cookies.get("sid")
|
||||
url = "https://api.deezer.com/1.0/gateway.php"
|
||||
|
||||
params = {
|
||||
'api_key': "4VCYIJUCDLOUELGD1V8WBVYBNVDYOXEWSLLZDONGBBDFVXTZJRXPR29JRLQFO6ZE",
|
||||
'sid': guest_sid,
|
||||
'input': '3',
|
||||
'output': '3',
|
||||
'method': 'song_getData'
|
||||
}
|
||||
|
||||
json = {'sng_id': 302127}
|
||||
|
||||
json = req_post(url, params=params, json=json).json()
|
||||
print(json)
|
||||
|
||||
def __get_api(
|
||||
self, method,
|
||||
json_data=None
|
||||
):
|
||||
params = {
|
||||
"api_version": "1.0",
|
||||
"api_token": self.__token,
|
||||
"input": "3",
|
||||
"method": method
|
||||
}
|
||||
|
||||
results = self.__req.post(
|
||||
self.__private_api_link,
|
||||
params=params,
|
||||
json=json_data
|
||||
).json()['results']
|
||||
|
||||
if not results:
|
||||
self.__refresh_token()
|
||||
self.__get_api(method, json_data)
|
||||
|
||||
return results
|
||||
|
||||
def get_user(self):
|
||||
data = self.__get_api(self.__get_user_data)
|
||||
return data
|
||||
|
||||
def __refresh_token(self):
|
||||
self.__req.cookies.clear_session_cookies()
|
||||
|
||||
if not self.amIlog():
|
||||
self.__login()
|
||||
self.am_I_log()
|
||||
|
||||
data = self.get_user()
|
||||
self.__token = data['checkForm']
|
||||
self.__license_token = self.__get_license_token()
|
||||
|
||||
def __get_license_token(self):
|
||||
data = self.get_user()
|
||||
license_token = data['USER']['OPTIONS']['license_token']
|
||||
|
||||
return license_token
|
||||
|
||||
def amIlog(self):
|
||||
data = self.get_user()
|
||||
user_id = data['USER']['USER_ID']
|
||||
is_logged = False
|
||||
|
||||
if user_id != 0:
|
||||
is_logged = True
|
||||
|
||||
return is_logged
|
||||
|
||||
def am_I_log(self):
|
||||
if not self.amIlog():
|
||||
raise BadCredentials(arl=self.__arl)
|
||||
|
||||
def get_song_data(self, ids):
|
||||
json_data = {
|
||||
"sng_id": ids
|
||||
}
|
||||
|
||||
infos = self.__get_api(self.__get_song_data, json_data)
|
||||
|
||||
return infos
|
||||
|
||||
def get_album_data(self, ids):
|
||||
json_data = {
|
||||
"alb_id": ids,
|
||||
"nb": -1
|
||||
}
|
||||
|
||||
infos = self.__get_api(self.__get_album_data, json_data)
|
||||
|
||||
return infos
|
||||
|
||||
def get_lyric(self, ids):
|
||||
json_data = {
|
||||
"sng_id": ids
|
||||
}
|
||||
|
||||
infos = self.__get_api(self.__get_lyric, json_data)
|
||||
|
||||
return infos
|
||||
|
||||
def get_playlist_data(self, ids):
|
||||
json_data = {
|
||||
"playlist_id": ids,
|
||||
"nb": -1
|
||||
}
|
||||
|
||||
infos = self.__get_api(self.__get_playlist_data, json_data)
|
||||
|
||||
return infos
|
||||
|
||||
def get_page_track(self, ids):
|
||||
json_data = {
|
||||
"sng_id": ids
|
||||
}
|
||||
|
||||
infos = self.__get_api(self.__get_page_track, json_data)
|
||||
|
||||
return infos
|
||||
|
||||
def get_song_url(self, n, song_hash):
|
||||
song_url = self.__song_server.format(n, song_hash)
|
||||
|
||||
return song_url
|
||||
|
||||
def song_exist(self, song_url):
|
||||
crypted_audio = req_get(song_url)
|
||||
|
||||
if len(crypted_audio.content) == 0:
|
||||
raise TrackNotFound
|
||||
|
||||
return crypted_audio
|
||||
|
||||
def get_medias_url(self, tracks_token, quality):
|
||||
others_qualities = []
|
||||
|
||||
for c_quality in qualities:
|
||||
if c_quality == quality:
|
||||
continue
|
||||
|
||||
c_quality_set = {
|
||||
"cipher": "BF_CBC_STRIPE",
|
||||
"format": c_quality
|
||||
}
|
||||
|
||||
others_qualities.append(c_quality_set)
|
||||
|
||||
json_data = {
|
||||
"license_token": self.__license_token,
|
||||
"media": [
|
||||
{
|
||||
"type": "FULL",
|
||||
"formats": [
|
||||
{
|
||||
"cipher": "BF_CBC_STRIPE",
|
||||
"format": quality
|
||||
}
|
||||
] # + others_qualities
|
||||
}
|
||||
],
|
||||
"track_tokens": tracks_token
|
||||
}
|
||||
|
||||
infos = req_post(
|
||||
self.__get_media_url,
|
||||
json=json_data
|
||||
).json()
|
||||
|
||||
if "errors" in infos:
|
||||
msg = infos['errors'][0]['message']
|
||||
|
||||
raise NoRightOnMedia(msg)
|
||||
|
||||
medias = infos['data']
|
||||
|
||||
return medias
|
Binary file not shown.
@ -1,6 +0,0 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
from .album import Album
|
||||
from .playlist import Playlist
|
||||
from .preferences import Preferences
|
||||
from .track import Track
|
Binary file not shown.
@ -1,22 +0,0 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
|
||||
class Album:
|
||||
def __init__(self, ids: int) -> None:
|
||||
self.__t_list = []
|
||||
self.zip_path = None
|
||||
self.image = None
|
||||
self.album_quality = None
|
||||
self.md5_image = None
|
||||
self.ids = ids
|
||||
self.nb_tracks = None
|
||||
self.album_name = None
|
||||
self.upc = None
|
||||
self.__set_album_md5()
|
||||
|
||||
@property
|
||||
def tracks(self):
|
||||
return self.__t_list
|
||||
|
||||
def __set_album_md5(self):
|
||||
self.album_md5 = f"album/{self.ids}"
|
Binary file not shown.
@ -1,11 +0,0 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
|
||||
class Playlist:
|
||||
def __init__(self, tracklist=None) -> None:
|
||||
self.__t_list = tracklist or []
|
||||
self.zip_path = None
|
||||
|
||||
@property
|
||||
def tracks(self):
|
||||
return self.__t_list
|
Binary file not shown.
@ -1,15 +0,0 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
class Preferences:
|
||||
def __init__(self) -> None:
|
||||
self.link = None
|
||||
self.song_metadata = None
|
||||
self.quality_download = None
|
||||
self.output_dir = None
|
||||
self.ids = None
|
||||
self.json_data = None
|
||||
self.recursive_quality = None
|
||||
self.recursive_download = None
|
||||
self.not_interface = None
|
||||
self.method_save = None
|
||||
self.make_zip = None
|
Binary file not shown.
@ -1,61 +0,0 @@
|
||||
#!/usr/bin/python3
|
||||
from tempfile import gettempdir
|
||||
from os.path import isfile
|
||||
|
||||
|
||||
class Track:
|
||||
def __init__(
|
||||
self,
|
||||
tags: dict,
|
||||
song_path: str,
|
||||
file_format: str,
|
||||
quality: str,
|
||||
link: str,
|
||||
ids: int
|
||||
) -> None:
|
||||
self.tags = tags
|
||||
self.__set_tags()
|
||||
self.song_name = f"{self.music} - {self.artist}"
|
||||
self.song_path = song_path
|
||||
self.file_format = file_format
|
||||
self.quality = quality
|
||||
self.link = link
|
||||
self.ids = ids
|
||||
self.md5_image = None
|
||||
self.success = True
|
||||
self.__set_track_md5()
|
||||
|
||||
@property
|
||||
def image_path(self):
|
||||
path = self.song_path + ".jpg"
|
||||
if not isfile(path):
|
||||
try:
|
||||
with open(path, "wb") as f:
|
||||
f.write(self.tags["image"])
|
||||
except:
|
||||
pass
|
||||
return path
|
||||
|
||||
@property
|
||||
def track_info(self):
|
||||
return {
|
||||
"title": self.tags.get("music") or self.song_name,
|
||||
"url": self.link,
|
||||
"album": self.tags.get("album"),
|
||||
"genre": self.tags.get("genre"),
|
||||
"artist": self.tags.get("artist"),
|
||||
"duration": self.tags.get("duration", 0)
|
||||
}
|
||||
|
||||
def __set_tags(self):
|
||||
for tag, value in self.tags.items():
|
||||
setattr(
|
||||
self, tag, value
|
||||
)
|
||||
|
||||
def __set_track_md5(self):
|
||||
self.track_md5 = f"track/{self.ids}"
|
||||
|
||||
def set_fallback_ids(self, fallback_ids):
|
||||
self.fallback_ids = fallback_ids
|
||||
self.fallback_track_md5 = f"track/{self.fallback_ids}"
|
Binary file not shown.
@ -1,28 +0,0 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
header = {
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:88.0) Gecko/20100101 Firefox/88.0",
|
||||
"Accept-Language": "en-US;q=0.5,en;q=0.3"
|
||||
}
|
||||
|
||||
method_saves = ["0", "1", "2"]
|
||||
|
||||
qualities = {
|
||||
"FLAC": {
|
||||
"n_quality": "9",
|
||||
"f_format": ".flac",
|
||||
"s_quality": "FLAC"
|
||||
},
|
||||
|
||||
"MP3_320": {
|
||||
"n_quality": "3",
|
||||
"f_format": ".mp3",
|
||||
"s_quality": "320"
|
||||
},
|
||||
|
||||
"MP3_128": {
|
||||
"n_quality": "1",
|
||||
"f_format": ".mp3",
|
||||
"s_quality": "128"
|
||||
}
|
||||
}
|
Binary file not shown.
@ -1,228 +0,0 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
from mutagen.flac import FLAC, Picture
|
||||
from mutagen.id3 import (
|
||||
ID3NoHeaderError,
|
||||
ID3, APIC, USLT, SYLT,
|
||||
COMM, TSRC, TRCK, TIT2,
|
||||
TLEN, TEXT, TCON, TALB, TBPM,
|
||||
TPE1, TYER, TDAT, TPOS, TPE2,
|
||||
TPUB, TCOP, TXXX, TCOM, IPLS
|
||||
)
|
||||
|
||||
from .models.track import Track
|
||||
|
||||
|
||||
def __write_flac(song, data):
|
||||
tag = FLAC(song)
|
||||
tag.delete()
|
||||
images = Picture()
|
||||
images.type = 3
|
||||
images.data = data['image']
|
||||
tag.clear_pictures()
|
||||
tag.add_picture(images)
|
||||
tag['lyrics'] = data['lyric']
|
||||
tag['artist'] = data['artist']
|
||||
tag['title'] = data['music']
|
||||
tag[
|
||||
'date'] = f"{data['year'].year}/{data['year'].month}/{data['year'].day}"
|
||||
tag['album'] = data['album']
|
||||
tag['tracknumber'] = f"{data['tracknum']}"
|
||||
tag['discnumber'] = f"{data['discnum']}"
|
||||
tag['genre'] = data['genre']
|
||||
tag['albumartist'] = data['ar_album']
|
||||
tag['author'] = data['author']
|
||||
tag['composer'] = data['composer']
|
||||
tag['copyright'] = data['copyright']
|
||||
tag['bpm'] = f"{data['bpm']}"
|
||||
tag['length'] = f"{data['duration']}"
|
||||
tag['organization'] = data['label']
|
||||
tag['isrc'] = data['isrc']
|
||||
tag['lyricist'] = data['lyricist']
|
||||
tag['version'] = data['version']
|
||||
tag.save()
|
||||
|
||||
|
||||
def __write_mp3(song, data):
|
||||
try:
|
||||
audio = ID3(song)
|
||||
audio.delete()
|
||||
except ID3NoHeaderError:
|
||||
audio = ID3()
|
||||
|
||||
audio.add(
|
||||
APIC(
|
||||
mime="image/jpeg",
|
||||
type=3,
|
||||
desc="album front cover",
|
||||
data=data['image']
|
||||
)
|
||||
)
|
||||
|
||||
audio.add(
|
||||
COMM(
|
||||
lang="eng",
|
||||
desc="my comment",
|
||||
text="DO NOT USE FOR YOUR OWN EARNING"
|
||||
)
|
||||
)
|
||||
|
||||
audio.add(
|
||||
USLT(
|
||||
text=data['lyric']
|
||||
)
|
||||
)
|
||||
|
||||
audio.add(
|
||||
SYLT(
|
||||
type=1,
|
||||
format=2,
|
||||
desc="sync lyric song",
|
||||
text=data['lyric_sync']
|
||||
)
|
||||
)
|
||||
|
||||
audio.add(
|
||||
TSRC(
|
||||
text=data['isrc']
|
||||
)
|
||||
)
|
||||
|
||||
audio.add(
|
||||
TRCK(
|
||||
text=f"{data['tracknum']}/{data['nb_tracks']}"
|
||||
)
|
||||
)
|
||||
|
||||
audio.add(
|
||||
TIT2(
|
||||
text=data['music']
|
||||
)
|
||||
)
|
||||
|
||||
audio.add(
|
||||
TLEN(
|
||||
text=f"{data['duration']}"
|
||||
)
|
||||
)
|
||||
|
||||
audio.add(
|
||||
TEXT(
|
||||
text=data['lyricist']
|
||||
)
|
||||
)
|
||||
|
||||
audio.add(
|
||||
TCON(
|
||||
text=data['genre']
|
||||
)
|
||||
)
|
||||
|
||||
audio.add(
|
||||
TALB(
|
||||
text=data['album']
|
||||
)
|
||||
)
|
||||
|
||||
audio.add(
|
||||
TBPM(
|
||||
text=f"{data['bpm']}"
|
||||
)
|
||||
)
|
||||
|
||||
audio.add(
|
||||
TPE1(
|
||||
text=data['artist']
|
||||
)
|
||||
)
|
||||
|
||||
audio.add(
|
||||
TYER(
|
||||
text=f"{data['year'].year}"
|
||||
)
|
||||
)
|
||||
|
||||
audio.add(
|
||||
TDAT(
|
||||
text=f"{data['year'].day}{data['year'].month}"
|
||||
)
|
||||
)
|
||||
|
||||
audio.add(
|
||||
TPOS(
|
||||
text=f"{data['discnum']}/{data['discnum']}"
|
||||
)
|
||||
)
|
||||
|
||||
audio.add(
|
||||
TPE2(
|
||||
text=data['ar_album']
|
||||
)
|
||||
)
|
||||
|
||||
audio.add(
|
||||
TPUB(
|
||||
text=data['label']
|
||||
)
|
||||
)
|
||||
|
||||
audio.add(
|
||||
TCOP(
|
||||
text=data['copyright']
|
||||
)
|
||||
)
|
||||
|
||||
audio.add(
|
||||
TXXX(
|
||||
desc="REPLAYGAIN_TRACK_GAIN",
|
||||
text=f"{data['gain']}"
|
||||
)
|
||||
)
|
||||
|
||||
audio.add(
|
||||
TCOM(
|
||||
text=data['composer']
|
||||
)
|
||||
)
|
||||
|
||||
audio.add(
|
||||
IPLS(
|
||||
people=[data['author']]
|
||||
)
|
||||
)
|
||||
|
||||
audio.save(song, v2_version=3)
|
||||
|
||||
|
||||
def write_tags(track: Track):
|
||||
song = track.song_path
|
||||
song_metadata = track.tags
|
||||
f_format = track.file_format
|
||||
|
||||
if f_format == ".flac":
|
||||
__write_flac(song, song_metadata)
|
||||
else:
|
||||
__write_mp3(song, song_metadata)
|
||||
|
||||
|
||||
def check_track(track: Track):
|
||||
song = track.song_path
|
||||
f_format = track.file_format
|
||||
is_ok = False
|
||||
|
||||
if f_format == ".flac":
|
||||
tags = FLAC(song)
|
||||
else:
|
||||
try:
|
||||
tags = ID3(song)
|
||||
except ID3NoHeaderError:
|
||||
return is_ok
|
||||
|
||||
l_tags = len(
|
||||
tags.keys()
|
||||
)
|
||||
|
||||
if l_tags > 15:
|
||||
is_ok = True
|
||||
|
||||
return is_ok
|
Binary file not shown.
@ -1,228 +0,0 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
from datetime import datetime
|
||||
from os import makedirs
|
||||
from os.path import (
|
||||
isdir, basename, join
|
||||
)
|
||||
from urllib.parse import urlparse
|
||||
from zipfile import ZipFile, ZIP_DEFLATED
|
||||
|
||||
from requests import get as req_get
|
||||
|
||||
from .settings import header
|
||||
from .exceptions import InvalidLink
|
||||
|
||||
|
||||
def link_is_valid(link):
|
||||
netloc = urlparse(link).netloc
|
||||
|
||||
if not any(
|
||||
c_link == netloc
|
||||
for c_link in ["www.deezer.com", "deezer.com", "deezer.page.link"]
|
||||
):
|
||||
raise InvalidLink(link)
|
||||
|
||||
|
||||
def get_ids(link):
|
||||
parsed = urlparse(link)
|
||||
path = parsed.path
|
||||
ids = path.split("/")[-1]
|
||||
return ids
|
||||
|
||||
|
||||
def request(url):
|
||||
thing = req_get(url, headers=header)
|
||||
return thing
|
||||
|
||||
|
||||
def artist_sort(array):
|
||||
if len(array) > 1:
|
||||
for a in array:
|
||||
for b in array:
|
||||
if a in b and a != b:
|
||||
array.remove(b)
|
||||
|
||||
array = list(
|
||||
dict.fromkeys(array)
|
||||
)
|
||||
|
||||
artists = " & ".join(array)
|
||||
return artists
|
||||
|
||||
|
||||
def __check_dir(directory):
|
||||
if not isdir(directory):
|
||||
makedirs(directory)
|
||||
|
||||
|
||||
def check_track_md5(infos: dict):
|
||||
if "FALLBACK" in infos:
|
||||
song_md5 = infos['FALLBACK']['MD5_ORIGIN']
|
||||
version = infos['FALLBACK']['MEDIA_VERSION']
|
||||
else:
|
||||
song_md5 = infos['MD5_ORIGIN']
|
||||
version = infos['MEDIA_VERSION']
|
||||
|
||||
return song_md5, version
|
||||
|
||||
|
||||
def check_track_token(infos: dict):
|
||||
if "FALLBACK" in infos:
|
||||
track_token = infos['FALLBACK']['TRACK_TOKEN']
|
||||
else:
|
||||
track_token = infos['TRACK_TOKEN']
|
||||
|
||||
return track_token
|
||||
|
||||
|
||||
def check_track_ids(infos: dict):
|
||||
if "FALLBACK" in infos:
|
||||
ids = infos['FALLBACK']['SNG_ID']
|
||||
else:
|
||||
ids = infos['SNG_ID']
|
||||
|
||||
return ids
|
||||
|
||||
|
||||
def __var_excape(string):
|
||||
string = (
|
||||
string
|
||||
.replace("\\", "")
|
||||
.replace("/", "")
|
||||
.replace(":", "")
|
||||
.replace("*", "")
|
||||
.replace("?", "")
|
||||
.replace("\"", "")
|
||||
.replace("<", "")
|
||||
.replace(">", "")
|
||||
.replace("|", "")
|
||||
.replace("&", "")
|
||||
)
|
||||
|
||||
return string
|
||||
|
||||
|
||||
def convert_to_date(date):
|
||||
if date == "0000-00-00":
|
||||
date = "0001-01-01"
|
||||
|
||||
date = datetime.strptime(date, "%Y-%m-%d")
|
||||
return date
|
||||
|
||||
|
||||
def what_kind(link):
|
||||
url = request(link).url
|
||||
return url
|
||||
|
||||
|
||||
def __get_dir(song_metadata, output_dir, method_save):
|
||||
album = __var_excape(song_metadata['album'])
|
||||
artist = __var_excape(song_metadata['ar_album'])
|
||||
upc = song_metadata['upc']
|
||||
|
||||
if method_save == 0:
|
||||
song_dir = f"{album} [{upc}]"
|
||||
|
||||
elif method_save == 1:
|
||||
song_dir = f"{album} - {artist}"
|
||||
|
||||
elif method_save == 2:
|
||||
song_dir = f"{album} - {artist} [{upc}]"
|
||||
|
||||
song_dir = song_dir[:255]
|
||||
final_dir = join(output_dir, song_dir)
|
||||
final_dir += "/"
|
||||
return final_dir
|
||||
|
||||
|
||||
def set_path(
|
||||
song_metadata, output_dir,
|
||||
song_quality, file_format, method_save
|
||||
):
|
||||
album = __var_excape(song_metadata['album'])
|
||||
artist = __var_excape(song_metadata['artist'])
|
||||
music = __var_excape(song_metadata['music'])
|
||||
|
||||
if method_save == 0:
|
||||
discnum = song_metadata['discnumber']
|
||||
tracknum = song_metadata['tracknumber']
|
||||
song_name = f"{album} CD {discnum} TRACK {tracknum}"
|
||||
|
||||
elif method_save == 1:
|
||||
song_name = f"{music} - {artist}"
|
||||
|
||||
elif method_save == 2:
|
||||
isrc = song_metadata['isrc']
|
||||
song_name = f"{music} - {artist} [{isrc}]"
|
||||
|
||||
song_dir = __get_dir(song_metadata, output_dir, method_save)
|
||||
__check_dir(song_dir)
|
||||
|
||||
l_encoded = len(
|
||||
song_name.encode()
|
||||
)
|
||||
|
||||
if l_encoded > 242:
|
||||
n_tronc = l_encoded - 242
|
||||
n_tronc = len(song_name) - n_tronc
|
||||
else:
|
||||
n_tronc = 242
|
||||
|
||||
song_path = f"{song_dir}{song_name[:n_tronc]}"
|
||||
song_path += f" ({song_quality}){file_format}"
|
||||
|
||||
return song_path
|
||||
|
||||
|
||||
def create_zip(
|
||||
tracks: [],
|
||||
output_dir=None,
|
||||
song_metadata=None,
|
||||
song_quality=None,
|
||||
method_save=0,
|
||||
zip_name=None
|
||||
):
|
||||
if not zip_name:
|
||||
album = __var_excape(song_metadata['album'])
|
||||
song_dir = __get_dir(song_metadata, output_dir, method_save)
|
||||
|
||||
if method_save == 0:
|
||||
zip_name = f"{song_dir}{album} ({song_quality})"
|
||||
|
||||
elif method_save == 1:
|
||||
artist = __var_excape(song_metadata['ar_album'])
|
||||
zip_name = f"{song_dir}{album} - {artist} ({song_quality})"
|
||||
|
||||
elif method_save == 2:
|
||||
artist = __var_excape(song_metadata['ar_album'])
|
||||
upc = song_metadata['upc']
|
||||
zip_name = f"{song_dir}{album} - {artist} {upc} ({song_quality})"
|
||||
|
||||
zip_name += ".zip"
|
||||
z = ZipFile(zip_name, "w", ZIP_DEFLATED)
|
||||
|
||||
for track in tracks:
|
||||
if not track.success:
|
||||
continue
|
||||
|
||||
c_song_path = track.song_path
|
||||
song_path = basename(c_song_path)
|
||||
z.write(c_song_path, song_path)
|
||||
|
||||
z.close()
|
||||
return zip_name
|
||||
|
||||
|
||||
def trasform_sync_lyric(lyric):
|
||||
sync_array = []
|
||||
|
||||
for a in lyric:
|
||||
if "milliseconds" in a:
|
||||
arr = (
|
||||
a['line'], int(a['milliseconds'])
|
||||
)
|
||||
|
||||
sync_array.append(arr)
|
||||
|
||||
return sync_array
|
Binary file not shown.
@ -1,3 +0,0 @@
|
||||
UNKNOWN
|
||||
|
||||
|
@ -1 +0,0 @@
|
||||
pip
|
@ -1,16 +0,0 @@
|
||||
Metadata-Version: 2.0
|
||||
Name: py-bandcamp
|
||||
Version: 0.7.0
|
||||
Summary: bandcamp data scrapper
|
||||
Home-page: https://github.com/OpenJarbas/py_bandcamp
|
||||
Author: jarbasAI
|
||||
Author-email: jarbasai@mailfence.com
|
||||
License: Apache2
|
||||
Platform: UNKNOWN
|
||||
Requires-Dist: beautifulsoup4
|
||||
Requires-Dist: requests
|
||||
Requires-Dist: requests-cache
|
||||
|
||||
UNKNOWN
|
||||
|
||||
|
@ -1,16 +0,0 @@
|
||||
py_bandcamp-0.7.0.dist-info/DESCRIPTION.rst,sha256=OCTuuN6LcWulhHS3d5rfjdsQtW22n7HENFRh6jC6ego,10
|
||||
py_bandcamp-0.7.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||
py_bandcamp-0.7.0.dist-info/METADATA,sha256=v64yE1qTYkPrI6J2MtCwmVQdOOZbFHrdr5vQC5guAmk,324
|
||||
py_bandcamp-0.7.0.dist-info/RECORD,,
|
||||
py_bandcamp-0.7.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
py_bandcamp-0.7.0.dist-info/WHEEL,sha256=rNo05PbNqwnXiIHFsYm0m22u4Zm6YJtugFG2THx4w3g,92
|
||||
py_bandcamp-0.7.0.dist-info/metadata.json,sha256=N3uWHAUUKCiP03daPAIJfc8j6sUY8GyWfXM1r4KBukg,512
|
||||
py_bandcamp-0.7.0.dist-info/top_level.txt,sha256=bbnj4Boxiv1B7Bo1GHfc5_jVs9p4egzSunq5FQtaCsQ,12
|
||||
py_bandcamp/__init__.py,sha256=NI84FGwg_EouRuSqRp4JsO5MvSKbuizjOzXsWlNo_R0,9801
|
||||
py_bandcamp/__init__.pyc,,
|
||||
py_bandcamp/models.py,sha256=bCz1oNwDEioNvMAzy8mVmX6bX7NhcjYVKew1_ncsajI,10877
|
||||
py_bandcamp/models.pyc,,
|
||||
py_bandcamp/session.py,sha256=j3KLPYMn5YV6rgZNNT9MFpLJJ374HBY0f3SDnTUPnjA,101
|
||||
py_bandcamp/session.pyc,,
|
||||
py_bandcamp/utils.py,sha256=k5tJESDviwz5wJawRqFR-99TOcYLwilUFInPfoa19oU,2076
|
||||
py_bandcamp/utils.pyc,,
|
@ -1,5 +0,0 @@
|
||||
Wheel-Version: 1.0
|
||||
Generator: bdist_wheel (0.29.0)
|
||||
Root-Is-Purelib: true
|
||||
Tag: py3-none-any
|
||||
|
@ -1 +0,0 @@
|
||||
{"extensions": {"python.details": {"contacts": [{"email": "jarbasai@mailfence.com", "name": "jarbasAI", "role": "author"}], "document_names": {"description": "DESCRIPTION.rst"}, "project_urls": {"Home": "https://github.com/OpenJarbas/py_bandcamp"}}}, "extras": [], "generator": "bdist_wheel (0.29.0)", "license": "Apache2", "metadata_version": "2.0", "name": "py-bandcamp", "run_requires": [{"requires": ["beautifulsoup4", "requests", "requests-cache"]}], "summary": "bandcamp data scrapper", "version": "0.7.0"}
|
@ -1 +0,0 @@
|
||||
py_bandcamp
|
@ -1,248 +0,0 @@
|
||||
from py_bandcamp.models import *
|
||||
from py_bandcamp.session import SESSION as requests
|
||||
from py_bandcamp.utils import extract_ldjson_blob, get_props, extract_blob, \
|
||||
get_stream_data
|
||||
|
||||
|
||||
class BandCamp:
|
||||
@staticmethod
|
||||
def tags(tag_list=True):
|
||||
data = extract_blob("https://bandcamp.com/tags")
|
||||
tags = {"genres": data["signup_params"]["genres"],
|
||||
"subgenres": data["signup_params"]["subgenres"]}
|
||||
if not tag_list:
|
||||
return tags
|
||||
tag_list = []
|
||||
for genre in tags["subgenres"]:
|
||||
tag_list.append(genre)
|
||||
tag_list += [sub["norm_name"] for sub in tags["subgenres"][genre]]
|
||||
return tag_list
|
||||
|
||||
@classmethod
|
||||
def search_tag(cls, tag, page=1, pop_date=1):
|
||||
tag = tag.strip().replace(" ", "-").lower()
|
||||
if tag not in cls.tags():
|
||||
return []
|
||||
params = {"page": page, "sort_field": pop_date}
|
||||
url = 'http://bandcamp.com/tag/' + str(tag)
|
||||
data = extract_blob(url, params=params)
|
||||
|
||||
related_tags = [{"name": t["norm_name"], "score": t["relation"]}
|
||||
for t in data["hub"].pop("related_tags")]
|
||||
|
||||
collections, dig_deeper = data["hub"].pop("tabs")
|
||||
dig_deeper = dig_deeper["dig_deeper"]["results"]
|
||||
collections = collections["collections"]
|
||||
|
||||
_to_remove = ['custom_domain', 'custom_domain_verified', "item_type",
|
||||
'packages', 'slug_text', 'subdomain', 'is_preorder',
|
||||
'item_id', 'num_comments', 'tralbum_id', 'band_id',
|
||||
'tralbum_type', 'tag_id', 'audio_track_id']
|
||||
|
||||
for c in collections:
|
||||
if c["name"] == "bc_dailys":
|
||||
continue
|
||||
for result in c["items"]:
|
||||
result["image"] = "https://f4.bcbits.com/img/a{art_id}_1.jpg". \
|
||||
format(art_id=result.pop("art_id"))
|
||||
for _ in _to_remove:
|
||||
if _ in result:
|
||||
result.pop(_)
|
||||
result["related_tags"] = related_tags
|
||||
result["collection"] = c["name"]
|
||||
if "tralbum_url" in result:
|
||||
result["album_url"] = result.pop("tralbum_url")
|
||||
# TODO featured track object
|
||||
yield BandcampTrack(result, parse=False)
|
||||
|
||||
for k in dig_deeper:
|
||||
for result in dig_deeper[k]["items"]:
|
||||
for _ in _to_remove:
|
||||
if _ in result:
|
||||
result.pop(_)
|
||||
result["related_tags"] = related_tags
|
||||
result["collection"] = "dig_deeper"
|
||||
if "tralbum_url" in result:
|
||||
result["album_url"] = result.pop("tralbum_url")
|
||||
# TODO featured track object
|
||||
yield BandcampTrack(result, parse=False)
|
||||
|
||||
@classmethod
|
||||
def search_albums(cls, album_name):
|
||||
for album in cls.search(album_name, albums=True, tracks=False,
|
||||
artists=False, labels=False):
|
||||
yield album
|
||||
|
||||
@classmethod
|
||||
def search_tracks(cls, track_name):
|
||||
for t in cls.search(track_name, albums=False, tracks=True,
|
||||
artists=False, labels=False):
|
||||
yield t
|
||||
|
||||
@classmethod
|
||||
def search_artists(cls, artist_name):
|
||||
for a in cls.search(artist_name, albums=False, tracks=False,
|
||||
artists=True, labels=False):
|
||||
yield a
|
||||
|
||||
@classmethod
|
||||
def search_labels(cls, label_name):
|
||||
for a in cls.search(label_name, albums=False, tracks=False,
|
||||
artists=False, labels=True):
|
||||
yield a
|
||||
|
||||
@classmethod
|
||||
def search(cls, name, page=1, albums=True, tracks=True, artists=True,
|
||||
labels=False):
|
||||
params = {"page": page, "q": name}
|
||||
response = requests.get('http://bandcamp.com/search', params=params)
|
||||
html_doc = response.content
|
||||
soup = BeautifulSoup(html_doc, 'html.parser')
|
||||
|
||||
seen = []
|
||||
for item in soup.find_all("li", class_="searchresult"):
|
||||
item_type = item.find('div',
|
||||
class_='itemtype').text.strip().lower()
|
||||
if item_type == "album" and albums:
|
||||
data = cls._parse_album(item)
|
||||
elif item_type == "track" and tracks:
|
||||
data = cls._parse_track(item)
|
||||
elif item_type == "artist" and artists:
|
||||
data = cls._parse_artist(item)
|
||||
elif item_type == "label" and labels:
|
||||
data = cls._parse_label(item)
|
||||
else:
|
||||
continue
|
||||
# data["type"] = type
|
||||
yield data
|
||||
seen.append(data)
|
||||
if not len(seen):
|
||||
return # no more pages
|
||||
for item in cls.search(name, page=page + 1, albums=albums,
|
||||
tracks=tracks, artists=artists,
|
||||
labels=labels):
|
||||
if item in seen:
|
||||
return # duplicate data, fail safe out of loops
|
||||
yield item
|
||||
|
||||
@staticmethod
|
||||
def get_track_lyrics(track_url):
|
||||
track_page = requests.get(track_url)
|
||||
track_soup = BeautifulSoup(track_page.text, 'html.parser')
|
||||
track_lyrics = track_soup.find("div", {"class": "lyricsText"})
|
||||
if track_lyrics:
|
||||
return track_lyrics.text
|
||||
return "lyrics unavailable"
|
||||
|
||||
@classmethod
|
||||
def get_streams(cls, urls):
|
||||
if not isinstance(urls, list):
|
||||
urls = [urls]
|
||||
direct_links = [cls.get_stream_url(url) for url in urls]
|
||||
return direct_links
|
||||
|
||||
@classmethod
|
||||
def get_stream_url(cls, url):
|
||||
data = get_stream_data(url)
|
||||
print(data)
|
||||
for p in data['additionalProperty']:
|
||||
if p['name'] == 'file_mp3-128':
|
||||
return p["value"]
|
||||
return url
|
||||
|
||||
@staticmethod
|
||||
def _parse_label(item):
|
||||
art = item.find("div", {"class": "art"}).find("img")
|
||||
if art:
|
||||
art = art["src"]
|
||||
name = item.find('div', class_='heading').text.strip()
|
||||
url = item.find(
|
||||
'div', class_='heading').find('a')['href'].split("?")[0]
|
||||
location = item.find('div', class_='subhead').text.strip()
|
||||
try:
|
||||
tags = item.find(
|
||||
'div', class_='tags').text.replace("tags:", "").split(",")
|
||||
tags = [t.strip().lower() for t in tags]
|
||||
except: # sometimes missing
|
||||
tags = []
|
||||
|
||||
data = {"name": name, "location": location,
|
||||
"tags": tags, "url": url, "image": art
|
||||
}
|
||||
return BandcampLabel(data)
|
||||
|
||||
@staticmethod
|
||||
def _parse_artist(item):
|
||||
name = item.find('div', class_='heading').text.strip()
|
||||
url = item.find(
|
||||
'div', class_='heading').find('a')['href'].split("?")[0]
|
||||
genre = item.find(
|
||||
'div', class_='genre').text.strip().replace("genre: ", "")
|
||||
location = item.find('div', class_='subhead').text.strip()
|
||||
try:
|
||||
tags = item.find(
|
||||
'div', class_='tags').text.replace("tags:", "").split(",")
|
||||
tags = [t.strip().lower() for t in tags]
|
||||
except: # sometimes missing
|
||||
tags = []
|
||||
art = item.find("div", {"class": "art"}).find("img")["src"]
|
||||
|
||||
data = {"name": name, "genre": genre, "location": location,
|
||||
"tags": tags, "url": url, "image": art, "albums": []
|
||||
}
|
||||
return BandcampArtist(data)
|
||||
|
||||
@staticmethod
|
||||
def _parse_track(item):
|
||||
track_name = item.find('div', class_='heading').text.strip()
|
||||
url = item.find(
|
||||
'div', class_='heading').find('a')['href'].split("?")[0]
|
||||
album_name, artist = item.find(
|
||||
'div', class_='subhead').text.strip().split("by")
|
||||
album_name = album_name.strip().replace("from ", "")
|
||||
artist = artist.strip()
|
||||
released = item.find(
|
||||
'div', class_='released').text.strip().replace("released ", "")
|
||||
try:
|
||||
tags = item.find(
|
||||
'div', class_='tags').text.replace("tags:", "").split(",")
|
||||
tags = [t.strip().lower() for t in tags]
|
||||
except: # sometimes missing
|
||||
tags = []
|
||||
|
||||
art = item.find("div", {"class": "art"}).find("img")["src"]
|
||||
data = {"track_name": track_name, "released": released, "url": url,
|
||||
"tags": tags, "album_name": album_name, "artist": artist,
|
||||
"image": art
|
||||
}
|
||||
return BandcampTrack(data)
|
||||
|
||||
@staticmethod
|
||||
def _parse_album(item):
|
||||
art = item.find("div", {"class": "art"}).find("img")["src"]
|
||||
album_name = item.find('div', class_='heading').text.strip()
|
||||
url = item.find(
|
||||
'div', class_='heading').find('a')['href'].split("?")[0]
|
||||
length = item.find('div', class_='length').text.strip()
|
||||
tracks, minutes = length.split(",")
|
||||
tracks = tracks.replace(" tracks", "").replace(" track", "").strip()
|
||||
minutes = minutes.replace(" minutes", "").strip()
|
||||
released = item.find(
|
||||
'div', class_='released').text.strip().replace("released ", "")
|
||||
tags = item.find(
|
||||
'div', class_='tags').text.replace("tags:", "").split(",")
|
||||
tags = [t.strip().lower() for t in tags]
|
||||
artist = item.find("div", {"class": "subhead"}).text.strip()
|
||||
if artist.startswith("by "):
|
||||
artist = artist[3:]
|
||||
data = {"album_name": album_name,
|
||||
"length": length,
|
||||
"minutes": minutes,
|
||||
"url": url,
|
||||
"image": art,
|
||||
"artist": artist,
|
||||
"track_number": tracks,
|
||||
"released": released,
|
||||
"tags": tags
|
||||
}
|
||||
return BandcampAlbum(data, scrap=False)
|
Binary file not shown.
@ -1,406 +0,0 @@
|
||||
from bs4 import BeautifulSoup
|
||||
from py_bandcamp.session import SESSION as requests
|
||||
from py_bandcamp.utils import extract_ldjson_blob, get_props
|
||||
|
||||
|
||||
class BandcampTrack:
|
||||
def __init__(self, data, parse=True):
|
||||
self._url = data.get("url")
|
||||
self._data = data or {}
|
||||
self._page_data = {}
|
||||
if parse:
|
||||
self.parse_page()
|
||||
if not self.url:
|
||||
raise ValueError("bandcamp url is not set")
|
||||
|
||||
def parse_page(self):
|
||||
self._page_data = self.get_track_data(self.url)
|
||||
return self._page_data
|
||||
|
||||
@staticmethod
|
||||
def from_url(url):
|
||||
return BandcampTrack({"url": url})
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
return self._url or self.data.get("url")
|
||||
|
||||
@property
|
||||
def album(self):
|
||||
return self.get_album(self.url)
|
||||
|
||||
@property
|
||||
def artist(self):
|
||||
return self.get_artist(self.url)
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
for k, v in self._page_data.items():
|
||||
self._data[k] = v
|
||||
return self._data
|
||||
|
||||
@property
|
||||
def title(self):
|
||||
return self.data.get("title") or self.data.get("name") or \
|
||||
self.url.split("/")[-1]
|
||||
|
||||
@property
|
||||
def image(self):
|
||||
return self.data.get("image")
|
||||
|
||||
@property
|
||||
def track_num(self):
|
||||
return self.data.get("tracknum")
|
||||
|
||||
@property
|
||||
def duration(self):
|
||||
return self.data.get("duration_secs") or 0
|
||||
|
||||
@property
|
||||
def stream(self):
|
||||
return self.data.get("file_mp3-128")
|
||||
|
||||
@staticmethod
|
||||
def get_album(url):
|
||||
data = extract_ldjson_blob(url, clean=True)
|
||||
if data.get('inAlbum'):
|
||||
return BandcampAlbum({
|
||||
"title": data['inAlbum'].get('name'),
|
||||
"url": data['inAlbum'].get('id', url).split("#")[0],
|
||||
'type': data['inAlbum'].get("type"),
|
||||
})
|
||||
|
||||
@staticmethod
|
||||
def get_artist(url):
|
||||
data = extract_ldjson_blob(url, clean=True)
|
||||
d = data.get("byArtist")
|
||||
if d:
|
||||
return BandcampArtist({
|
||||
"title": d.get('name'),
|
||||
"url": d.get('id', url).split("#")[0],
|
||||
'genre': d.get('genre'),
|
||||
"artist_type": d.get('type')
|
||||
}, scrap=False)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def get_track_data(url):
|
||||
data = extract_ldjson_blob(url, clean=True)
|
||||
kwords = data.get('keywords', "")
|
||||
if isinstance(kwords, str):
|
||||
kwords = kwords.split(", ")
|
||||
track = {
|
||||
'dateModified': data.get('dateModified'),
|
||||
'datePublished': data.get('datePublished'),
|
||||
"url": data.get('id') or url,
|
||||
"title": data.get("name"),
|
||||
"type": data.get("type"),
|
||||
'image': data.get('image'),
|
||||
'keywords': kwords
|
||||
}
|
||||
for k, v in get_props(data).items():
|
||||
track[k] = v
|
||||
return track
|
||||
|
||||
def __repr__(self):
|
||||
return self.__class__.__name__ + ":" + self.title
|
||||
|
||||
def __str__(self):
|
||||
return self.url
|
||||
|
||||
|
||||
class BandcampAlbum:
|
||||
def __init__(self, data, scrap=True):
|
||||
self._url = data.get("url")
|
||||
self._data = data or {}
|
||||
self._page_data = {}
|
||||
if scrap:
|
||||
self.scrap()
|
||||
if not self.url:
|
||||
raise ValueError("bandcamp url is not set")
|
||||
|
||||
def scrap(self):
|
||||
self._page_data = self.get_album_data(self.url)
|
||||
return self._page_data
|
||||
|
||||
@staticmethod
|
||||
def from_url(url):
|
||||
return BandcampAlbum({"url": url})
|
||||
|
||||
@property
|
||||
def image(self):
|
||||
return self.data.get("image")
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
return self._url or self.data.get("url")
|
||||
|
||||
@property
|
||||
def title(self):
|
||||
return self.data.get("title") or self.data.get("name") or \
|
||||
self.url.split("/")[-1]
|
||||
|
||||
@property
|
||||
def releases(self):
|
||||
return self.get_releases(self.url)
|
||||
|
||||
@property
|
||||
def artist(self):
|
||||
return self.get_artist(self.url)
|
||||
|
||||
@property
|
||||
def keywords(self):
|
||||
return self.data.get("keywords") or []
|
||||
|
||||
@property
|
||||
def tracks(self):
|
||||
return self.get_tracks(self.url)
|
||||
|
||||
@property
|
||||
def featured_track(self):
|
||||
if not len(self.tracks):
|
||||
return None
|
||||
num = self.data.get('featured_track_num', 1) or 1
|
||||
return self.tracks[int(num) - 1]
|
||||
|
||||
@property
|
||||
def comments(self):
|
||||
return self.get_comments(self.url)
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
for k, v in self._page_data.items():
|
||||
self._data[k] = v
|
||||
return self._data
|
||||
|
||||
@staticmethod
|
||||
def get_releases(url):
|
||||
data = extract_ldjson_blob(url, clean=True)
|
||||
releases = []
|
||||
for d in data.get("albumRelease", []):
|
||||
release = {
|
||||
"description": d.get("description"),
|
||||
'image': d.get('image'),
|
||||
"title": d.get('name'),
|
||||
"url": d.get('id', url).split("#")[0],
|
||||
'format': d.get('musicReleaseFormat'),
|
||||
}
|
||||
releases.append(release)
|
||||
return releases
|
||||
|
||||
@staticmethod
|
||||
def get_artist(url):
|
||||
data = extract_ldjson_blob(url, clean=True)
|
||||
d = data.get("byArtist")
|
||||
if d:
|
||||
return BandcampArtist({
|
||||
"description": d.get("description"),
|
||||
'image': d.get('image'),
|
||||
"title": d.get('name'),
|
||||
"url": d.get('id', url).split("#")[0],
|
||||
'genre': d.get('genre'),
|
||||
"artist_type": d.get('type')
|
||||
}, scrap=False)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def get_tracks(url):
|
||||
data = extract_ldjson_blob(url, clean=True)
|
||||
if not data.get("track"):
|
||||
return []
|
||||
|
||||
data = data['track']
|
||||
|
||||
tracks = []
|
||||
|
||||
for d in data.get('itemListElement', []):
|
||||
d = d['item']
|
||||
track = {
|
||||
"title": d.get('name'),
|
||||
"url": d.get('id') or url,
|
||||
'type': d.get('type'),
|
||||
}
|
||||
for k, v in get_props(d).items():
|
||||
track[k] = v
|
||||
tracks.append(BandcampTrack(track, parse=False))
|
||||
return tracks
|
||||
|
||||
@staticmethod
|
||||
def get_comments(url):
|
||||
data = extract_ldjson_blob(url, clean=True)
|
||||
comments = []
|
||||
for d in data.get("comment", []):
|
||||
comment = {
|
||||
"text": d["text"],
|
||||
'image': d["author"].get("image"),
|
||||
"author": d["author"]["name"]
|
||||
}
|
||||
comments.append(comment)
|
||||
return comments
|
||||
|
||||
@staticmethod
|
||||
def get_album_data(url):
|
||||
data = extract_ldjson_blob(url, clean=True)
|
||||
props = get_props(data)
|
||||
kwords = data.get('keywords', "")
|
||||
if isinstance(kwords, str):
|
||||
kwords = kwords.split(", ")
|
||||
return {
|
||||
'dateModified': data.get('dateModified'),
|
||||
'datePublished': data.get('datePublished'),
|
||||
'description': data.get('description'),
|
||||
"url": data.get('id') or url,
|
||||
"title": data.get("name"),
|
||||
"type": data.get("type"),
|
||||
"n_tracks": data.get('numTracks'),
|
||||
'image': data.get('image'),
|
||||
'featured_track_num': props.get('featured_track_num'),
|
||||
'keywords': kwords
|
||||
}
|
||||
|
||||
def __repr__(self):
|
||||
return self.__class__.__name__ + ":" + self.title
|
||||
|
||||
def __str__(self):
|
||||
return self.url
|
||||
|
||||
|
||||
class BandcampLabel:
|
||||
def __init__(self, data, scrap=True):
|
||||
self._url = data.get("url")
|
||||
self._data = data or {}
|
||||
self._page_data = {}
|
||||
if scrap:
|
||||
self.scrap()
|
||||
if not self.url:
|
||||
raise ValueError("bandcamp url is not set")
|
||||
|
||||
def scrap(self):
|
||||
self._page_data = {} # TODO
|
||||
return self._page_data
|
||||
|
||||
@staticmethod
|
||||
def from_url(url):
|
||||
return BandcampTrack({"url": url})
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
return self._url or self.data.get("url")
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
for k, v in self._page_data.items():
|
||||
self._data[k] = v
|
||||
return self._data
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self.data.get("title") or self.data.get("name") or \
|
||||
self.url.split("/")[-1]
|
||||
|
||||
@property
|
||||
def location(self):
|
||||
return self.data.get("location")
|
||||
|
||||
@property
|
||||
def tags(self):
|
||||
return self.data.get("tags") or []
|
||||
|
||||
@property
|
||||
def image(self):
|
||||
return self.data.get("image")
|
||||
|
||||
def __repr__(self):
|
||||
return self.__class__.__name__ + ":" + self.name
|
||||
|
||||
def __str__(self):
|
||||
return self.url
|
||||
|
||||
|
||||
class BandcampArtist:
|
||||
def __init__(self, data, scrap=True):
|
||||
self._url = data.get("url")
|
||||
self._data = data or {}
|
||||
self._page_data = {}
|
||||
if scrap:
|
||||
self.scrap()
|
||||
|
||||
def scrap(self):
|
||||
self._page_data = {} # TODO
|
||||
return self._page_data
|
||||
|
||||
@property
|
||||
def featured_album(self):
|
||||
return BandcampAlbum.from_url(self.url + "/releases")
|
||||
|
||||
@property
|
||||
def featured_track(self):
|
||||
if not self.featured_album:
|
||||
return None
|
||||
return self.featured_album.featured_track
|
||||
|
||||
@staticmethod
|
||||
def get_albums(url):
|
||||
albums = []
|
||||
soup = BeautifulSoup(requests.get(url).text, "html.parser")
|
||||
for album in soup.find_all("a"):
|
||||
album_url = album.find("p", {"class": "title"})
|
||||
if album_url:
|
||||
title = album_url.text.strip()
|
||||
art = album.find("div", {"class": "art"}).find("img")["src"]
|
||||
album_url = url + album["href"]
|
||||
album = BandcampAlbum({"album_name": title,
|
||||
"image": art,
|
||||
"url": album_url})
|
||||
albums.append(album)
|
||||
return albums
|
||||
|
||||
@property
|
||||
def albums(self):
|
||||
return self.get_albums(self.url)
|
||||
|
||||
@staticmethod
|
||||
def from_url(url):
|
||||
return BandcampTrack({"url": url})
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
return self._url or self.data.get("url")
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
for k, v in self._page_data.items():
|
||||
self._data[k] = v
|
||||
return self._data
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self.data.get("title") or self.data.get("name") or \
|
||||
self.url.split("/")[-1]
|
||||
|
||||
@property
|
||||
def location(self):
|
||||
return self.data.get("location")
|
||||
|
||||
@property
|
||||
def genre(self):
|
||||
return self.data.get("genre")
|
||||
|
||||
@property
|
||||
def tags(self):
|
||||
return self.data.get("tags") or []
|
||||
|
||||
@property
|
||||
def image(self):
|
||||
return self.data.get("image")
|
||||
|
||||
def __repr__(self):
|
||||
return self.__class__.__name__ + ":" + self.name
|
||||
|
||||
def __str__(self):
|
||||
return self.url
|
||||
|
||||
def __eq__(self, other):
|
||||
if str(self) == str(other):
|
||||
return True
|
||||
return False
|
Binary file not shown.
@ -1,3 +0,0 @@
|
||||
import requests_cache
|
||||
|
||||
SESSION = requests_cache.CachedSession(expire_after=5 * 60, backend="memory")
|
Binary file not shown.
@ -1,77 +0,0 @@
|
||||
import json
|
||||
|
||||
from py_bandcamp.session import SESSION as requests
|
||||
|
||||
|
||||
def extract_blob(url, params=None):
|
||||
blob = requests.get(url, params=params).text
|
||||
for b in blob.split("data-blob='")[1:]:
|
||||
json_blob = b.split("'")[0]
|
||||
return json.loads(json_blob)
|
||||
for b in blob.split("data-blob=\"")[1:]:
|
||||
json_blob = b.split("\"")[0].replace(""", '"')
|
||||
return json.loads(json_blob)
|
||||
|
||||
|
||||
def extract_ldjson_blob(url, clean=False):
|
||||
txt_string = requests.get(url).text
|
||||
|
||||
json_blob = txt_string. \
|
||||
split('<script type="application/ld+json">')[-1]. \
|
||||
split("</script>")[0]
|
||||
|
||||
data = json.loads(json_blob)
|
||||
|
||||
def _clean_list(l):
|
||||
for idx, v in enumerate(l):
|
||||
if isinstance(v, dict):
|
||||
l[idx] = _clean_dict(v)
|
||||
if isinstance(v, list):
|
||||
l[idx] = _clean_list(v)
|
||||
return l
|
||||
|
||||
def _clean_dict(d):
|
||||
clean = {}
|
||||
for k, v in d.items():
|
||||
if isinstance(v, dict):
|
||||
v = _clean_dict(v)
|
||||
if isinstance(v, list):
|
||||
v = _clean_list(v)
|
||||
k = k.replace("@", "")
|
||||
clean[k] = v
|
||||
return clean
|
||||
|
||||
if clean:
|
||||
return _clean_dict(data)
|
||||
return data
|
||||
|
||||
|
||||
def get_props(d, props=None):
|
||||
props = props or []
|
||||
data = {}
|
||||
for p in d['additionalProperty']:
|
||||
if p['name'] in props or not props:
|
||||
data[p['name']] = p['value']
|
||||
return data
|
||||
|
||||
|
||||
def get_stream_data(url):
|
||||
data = extract_ldjson_blob(url)
|
||||
artist_data = data['byArtist']
|
||||
album_data = data['inAlbum']
|
||||
kws = data["keywords"]
|
||||
if isinstance(kws, str):
|
||||
kws = kws.split(", ")
|
||||
result = {
|
||||
"categories": data["@type"],
|
||||
'album_name': album_data['name'],
|
||||
'artist': artist_data['name'],
|
||||
'image': data['image'],
|
||||
"title": data['name'],
|
||||
"url": url,
|
||||
"tags": kws + data.get("tags", [])
|
||||
}
|
||||
for p in data['additionalProperty']:
|
||||
if p['name'] == 'file_mp3-128':
|
||||
result["stream"] = p["value"]
|
||||
return result
|
Binary file not shown.
@ -1 +0,0 @@
|
||||
pip
|
@ -1,28 +0,0 @@
|
||||
Copyright (c) 2008-2022, James Bennett
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following
|
||||
disclaimer in the documentation and/or other materials provided
|
||||
with the distribution.
|
||||
* Neither the name of the author nor the names of other
|
||||
contributors may be used to endorse or promote products derived
|
||||
from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
@ -1,64 +0,0 @@
|
||||
Metadata-Version: 2.1
|
||||
Name: webcolors
|
||||
Version: 1.12
|
||||
Summary: A library for working with color names and color values formats defined by HTML and CSS.
|
||||
Home-page: https://github.com/ubernostrum/webcolors
|
||||
Author: James Bennett
|
||||
Author-email: james@b-list.org
|
||||
License: BSD 3-Clause
|
||||
Classifier: Development Status :: 5 - Production/Stable
|
||||
Classifier: Environment :: Web Environment
|
||||
Classifier: Intended Audience :: Developers
|
||||
Classifier: License :: OSI Approved :: BSD License
|
||||
Classifier: Operating System :: OS Independent
|
||||
Classifier: Programming Language :: Python
|
||||
Classifier: Programming Language :: Python :: 3
|
||||
Classifier: Programming Language :: Python :: 3.7
|
||||
Classifier: Programming Language :: Python :: 3.8
|
||||
Classifier: Programming Language :: Python :: 3.9
|
||||
Classifier: Programming Language :: Python :: 3.10
|
||||
Classifier: Topic :: Utilities
|
||||
Requires-Python: >=3.7
|
||||
License-File: LICENSE
|
||||
|
||||
.. -*-restructuredtext-*-
|
||||
|
||||
.. image:: https://github.com/ubernostrum/webcolors/workflows/CI/badge.svg
|
||||
:alt: CI status image
|
||||
:target: https://github.com/ubernostrum/webcolors/actions?query=workflow%3ACI
|
||||
|
||||
``webcolors`` is a module for working with HTML/CSS color definitions.
|
||||
|
||||
Support is included for normalizing and converting between the
|
||||
following formats (RGB colorspace only; conversion to/from HSL can be
|
||||
handled by the ``colorsys`` module in the Python standard library):
|
||||
|
||||
* Specification-defined color names
|
||||
|
||||
* Six-digit hexadecimal
|
||||
|
||||
* Three-digit hexadecimal
|
||||
|
||||
* Integer ``rgb()`` triplet
|
||||
|
||||
* Percentage ``rgb()`` triplet
|
||||
|
||||
For example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
>>> import webcolors
|
||||
>>> webcolors.hex_to_name(u'#daa520')
|
||||
u'goldenrod'
|
||||
|
||||
Implementations are also provided for the HTML5 color parsing and
|
||||
serialization algorithms. For example, parsing the infamous
|
||||
"chucknorris" string into an rgb() triplet:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
>>> import webcolors
|
||||
>>> webcolors.html5_parse_legacy_color(u'chucknorris')
|
||||
HTML5SimpleColor(red=192, green=0, blue=0)
|
||||
|
||||
Full documentation is `available online <https://webcolors.readthedocs.io/>`_.
|
@ -1,9 +0,0 @@
|
||||
webcolors-1.12.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||
webcolors-1.12.dist-info/LICENSE,sha256=ii0_r1bvLUKXO599sXarGslRtQGkmhx7s0ACbx5NxIk,1523
|
||||
webcolors-1.12.dist-info/METADATA,sha256=56Xnd_OybLPtSNbCG8SixpiFKH0lqki7ihJWET3Ea_o,2049
|
||||
webcolors-1.12.dist-info/RECORD,,
|
||||
webcolors-1.12.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
webcolors-1.12.dist-info/WHEEL,sha256=_NOXIqFgOaYmlm9RJLPQZ13BJuEIrp5jx5ptRD5uh3Y,92
|
||||
webcolors-1.12.dist-info/top_level.txt,sha256=HUENGOTrUyEUebL1YSZy2ROMReykfiyMzSB-mSi72_4,10
|
||||
webcolors.py,sha256=OFDSm2rv4D0Jh1qhIOCLTy3g_8SpKfWNByWbnPmPYbk,25487
|
||||
webcolors.pyc,,
|
@ -1,5 +0,0 @@
|
||||
Wheel-Version: 1.0
|
||||
Generator: bdist_wheel (0.32.3)
|
||||
Root-Is-Purelib: true
|
||||
Tag: py3-none-any
|
||||
|
@ -1 +0,0 @@
|
||||
webcolors
|
@ -1,781 +0,0 @@
|
||||
"""
|
||||
Utility functions for working with the color names and color value
|
||||
formats defined by the HTML and CSS specifications for use in
|
||||
documents on the Web.
|
||||
|
||||
See documentation (in docs/ directory of source distribution) for
|
||||
details of the supported formats, conventions and conversions.
|
||||
|
||||
"""
|
||||
|
||||
import re
|
||||
import string
|
||||
from typing import NamedTuple, Tuple, Union
|
||||
|
||||
__version__ = "1.11.1"
|
||||
|
||||
|
||||
def _reversedict(d: dict) -> dict:
|
||||
"""
|
||||
Internal helper for generating reverse mappings; given a
|
||||
dictionary, returns a new dictionary with keys and values swapped.
|
||||
|
||||
"""
|
||||
return {value: key for key, value in d.items()}
|
||||
|
||||
|
||||
HEX_COLOR_RE = re.compile(r"^#([a-fA-F0-9]{3}|[a-fA-F0-9]{6})$")
|
||||
|
||||
HTML4 = "html4"
|
||||
CSS2 = "css2"
|
||||
CSS21 = "css21"
|
||||
CSS3 = "css3"
|
||||
|
||||
SUPPORTED_SPECIFICATIONS = (HTML4, CSS2, CSS21, CSS3)
|
||||
|
||||
SPECIFICATION_ERROR_TEMPLATE = "{{spec}} is not a supported specification for color name lookups; \
|
||||
supported specifications are: {supported}.".format(
|
||||
supported=",".join(SUPPORTED_SPECIFICATIONS)
|
||||
)
|
||||
|
||||
IntegerRGB = NamedTuple("IntegerRGB", [("red", int), ("green", int), ("blue", int)])
|
||||
PercentRGB = NamedTuple("PercentRGB", [("red", str), ("green", str), ("blue", str)])
|
||||
HTML5SimpleColor = NamedTuple(
|
||||
"HTML5SimpleColor", [("red", int), ("green", int), ("blue", int)]
|
||||
)
|
||||
|
||||
IntTuple = Union[IntegerRGB, HTML5SimpleColor, Tuple[int, int, int]]
|
||||
PercentTuple = Union[PercentRGB, Tuple[str, str, str]]
|
||||
|
||||
|
||||
# Mappings of color names to normalized hexadecimal color values.
|
||||
#################################################################
|
||||
|
||||
# The HTML 4 named colors.
|
||||
#
|
||||
# The canonical source for these color definitions is the HTML 4
|
||||
# specification:
|
||||
#
|
||||
# http://www.w3.org/TR/html401/types.html#h-6.5
|
||||
#
|
||||
# The file tests/definitions.py in the source distribution of this
|
||||
# module downloads a copy of the HTML 4 standard and parses out the
|
||||
# color names to ensure the values below are correct.
|
||||
HTML4_NAMES_TO_HEX = {
|
||||
"aqua": "#00ffff",
|
||||
"black": "#000000",
|
||||
"blue": "#0000ff",
|
||||
"fuchsia": "#ff00ff",
|
||||
"green": "#008000",
|
||||
"gray": "#808080",
|
||||
"lime": "#00ff00",
|
||||
"maroon": "#800000",
|
||||
"navy": "#000080",
|
||||
"olive": "#808000",
|
||||
"purple": "#800080",
|
||||
"red": "#ff0000",
|
||||
"silver": "#c0c0c0",
|
||||
"teal": "#008080",
|
||||
"white": "#ffffff",
|
||||
"yellow": "#ffff00",
|
||||
}
|
||||
|
||||
# CSS 2 used the same list as HTML 4.
|
||||
CSS2_NAMES_TO_HEX = HTML4_NAMES_TO_HEX
|
||||
|
||||
# CSS 2.1 added orange.
|
||||
CSS21_NAMES_TO_HEX = {"orange": "#ffa500", **HTML4_NAMES_TO_HEX}
|
||||
|
||||
# The CSS 3/SVG named colors.
|
||||
#
|
||||
# The canonical source for these color definitions is the SVG
|
||||
# specification's color list (which was adopted as CSS 3's color
|
||||
# definition):
|
||||
#
|
||||
# http://www.w3.org/TR/SVG11/types.html#ColorKeywords
|
||||
#
|
||||
# CSS 3 also provides definitions of these colors:
|
||||
#
|
||||
# http://www.w3.org/TR/css3-color/#svg-color
|
||||
#
|
||||
# SVG provides the definitions as RGB triplets. CSS 3 provides them
|
||||
# both as RGB triplets and as hexadecimal. Since hex values are more
|
||||
# common in real-world HTML and CSS, the mapping below is to hex
|
||||
# values instead. The file tests/definitions.py in the source
|
||||
# distribution of this module downloads a copy of the CSS 3 color
|
||||
# module and parses out the color names to ensure the values below are
|
||||
# correct.
|
||||
CSS3_NAMES_TO_HEX = {
|
||||
"aliceblue": "#f0f8ff",
|
||||
"antiquewhite": "#faebd7",
|
||||
"aqua": "#00ffff",
|
||||
"aquamarine": "#7fffd4",
|
||||
"azure": "#f0ffff",
|
||||
"beige": "#f5f5dc",
|
||||
"bisque": "#ffe4c4",
|
||||
"black": "#000000",
|
||||
"blanchedalmond": "#ffebcd",
|
||||
"blue": "#0000ff",
|
||||
"blueviolet": "#8a2be2",
|
||||
"brown": "#a52a2a",
|
||||
"burlywood": "#deb887",
|
||||
"cadetblue": "#5f9ea0",
|
||||
"chartreuse": "#7fff00",
|
||||
"chocolate": "#d2691e",
|
||||
"coral": "#ff7f50",
|
||||
"cornflowerblue": "#6495ed",
|
||||
"cornsilk": "#fff8dc",
|
||||
"crimson": "#dc143c",
|
||||
"cyan": "#00ffff",
|
||||
"darkblue": "#00008b",
|
||||
"darkcyan": "#008b8b",
|
||||
"darkgoldenrod": "#b8860b",
|
||||
"darkgray": "#a9a9a9",
|
||||
"darkgrey": "#a9a9a9",
|
||||
"darkgreen": "#006400",
|
||||
"darkkhaki": "#bdb76b",
|
||||
"darkmagenta": "#8b008b",
|
||||
"darkolivegreen": "#556b2f",
|
||||
"darkorange": "#ff8c00",
|
||||
"darkorchid": "#9932cc",
|
||||
"darkred": "#8b0000",
|
||||
"darksalmon": "#e9967a",
|
||||
"darkseagreen": "#8fbc8f",
|
||||
"darkslateblue": "#483d8b",
|
||||
"darkslategray": "#2f4f4f",
|
||||
"darkslategrey": "#2f4f4f",
|
||||
"darkturquoise": "#00ced1",
|
||||
"darkviolet": "#9400d3",
|
||||
"deeppink": "#ff1493",
|
||||
"deepskyblue": "#00bfff",
|
||||
"dimgray": "#696969",
|
||||
"dimgrey": "#696969",
|
||||
"dodgerblue": "#1e90ff",
|
||||
"firebrick": "#b22222",
|
||||
"floralwhite": "#fffaf0",
|
||||
"forestgreen": "#228b22",
|
||||
"fuchsia": "#ff00ff",
|
||||
"gainsboro": "#dcdcdc",
|
||||
"ghostwhite": "#f8f8ff",
|
||||
"gold": "#ffd700",
|
||||
"goldenrod": "#daa520",
|
||||
"gray": "#808080",
|
||||
"grey": "#808080",
|
||||
"green": "#008000",
|
||||
"greenyellow": "#adff2f",
|
||||
"honeydew": "#f0fff0",
|
||||
"hotpink": "#ff69b4",
|
||||
"indianred": "#cd5c5c",
|
||||
"indigo": "#4b0082",
|
||||
"ivory": "#fffff0",
|
||||
"khaki": "#f0e68c",
|
||||
"lavender": "#e6e6fa",
|
||||
"lavenderblush": "#fff0f5",
|
||||
"lawngreen": "#7cfc00",
|
||||
"lemonchiffon": "#fffacd",
|
||||
"lightblue": "#add8e6",
|
||||
"lightcoral": "#f08080",
|
||||
"lightcyan": "#e0ffff",
|
||||
"lightgoldenrodyellow": "#fafad2",
|
||||
"lightgray": "#d3d3d3",
|
||||
"lightgrey": "#d3d3d3",
|
||||
"lightgreen": "#90ee90",
|
||||
"lightpink": "#ffb6c1",
|
||||
"lightsalmon": "#ffa07a",
|
||||
"lightseagreen": "#20b2aa",
|
||||
"lightskyblue": "#87cefa",
|
||||
"lightslategray": "#778899",
|
||||
"lightslategrey": "#778899",
|
||||
"lightsteelblue": "#b0c4de",
|
||||
"lightyellow": "#ffffe0",
|
||||
"lime": "#00ff00",
|
||||
"limegreen": "#32cd32",
|
||||
"linen": "#faf0e6",
|
||||
"magenta": "#ff00ff",
|
||||
"maroon": "#800000",
|
||||
"mediumaquamarine": "#66cdaa",
|
||||
"mediumblue": "#0000cd",
|
||||
"mediumorchid": "#ba55d3",
|
||||
"mediumpurple": "#9370db",
|
||||
"mediumseagreen": "#3cb371",
|
||||
"mediumslateblue": "#7b68ee",
|
||||
"mediumspringgreen": "#00fa9a",
|
||||
"mediumturquoise": "#48d1cc",
|
||||
"mediumvioletred": "#c71585",
|
||||
"midnightblue": "#191970",
|
||||
"mintcream": "#f5fffa",
|
||||
"mistyrose": "#ffe4e1",
|
||||
"moccasin": "#ffe4b5",
|
||||
"navajowhite": "#ffdead",
|
||||
"navy": "#000080",
|
||||
"oldlace": "#fdf5e6",
|
||||
"olive": "#808000",
|
||||
"olivedrab": "#6b8e23",
|
||||
"orange": "#ffa500",
|
||||
"orangered": "#ff4500",
|
||||
"orchid": "#da70d6",
|
||||
"palegoldenrod": "#eee8aa",
|
||||
"palegreen": "#98fb98",
|
||||
"paleturquoise": "#afeeee",
|
||||
"palevioletred": "#db7093",
|
||||
"papayawhip": "#ffefd5",
|
||||
"peachpuff": "#ffdab9",
|
||||
"peru": "#cd853f",
|
||||
"pink": "#ffc0cb",
|
||||
"plum": "#dda0dd",
|
||||
"powderblue": "#b0e0e6",
|
||||
"purple": "#800080",
|
||||
"red": "#ff0000",
|
||||
"rosybrown": "#bc8f8f",
|
||||
"royalblue": "#4169e1",
|
||||
"saddlebrown": "#8b4513",
|
||||
"salmon": "#fa8072",
|
||||
"sandybrown": "#f4a460",
|
||||
"seagreen": "#2e8b57",
|
||||
"seashell": "#fff5ee",
|
||||
"sienna": "#a0522d",
|
||||
"silver": "#c0c0c0",
|
||||
"skyblue": "#87ceeb",
|
||||
"slateblue": "#6a5acd",
|
||||
"slategray": "#708090",
|
||||
"slategrey": "#708090",
|
||||
"snow": "#fffafa",
|
||||
"springgreen": "#00ff7f",
|
||||
"steelblue": "#4682b4",
|
||||
"tan": "#d2b48c",
|
||||
"teal": "#008080",
|
||||
"thistle": "#d8bfd8",
|
||||
"tomato": "#ff6347",
|
||||
"turquoise": "#40e0d0",
|
||||
"violet": "#ee82ee",
|
||||
"wheat": "#f5deb3",
|
||||
"white": "#ffffff",
|
||||
"whitesmoke": "#f5f5f5",
|
||||
"yellow": "#ffff00",
|
||||
"yellowgreen": "#9acd32",
|
||||
}
|
||||
|
||||
|
||||
# Mappings of normalized hexadecimal color values to color names.
|
||||
#################################################################
|
||||
|
||||
HTML4_HEX_TO_NAMES = _reversedict(HTML4_NAMES_TO_HEX)
|
||||
|
||||
CSS2_HEX_TO_NAMES = HTML4_HEX_TO_NAMES
|
||||
|
||||
CSS21_HEX_TO_NAMES = _reversedict(CSS21_NAMES_TO_HEX)
|
||||
|
||||
CSS3_HEX_TO_NAMES = _reversedict(CSS3_NAMES_TO_HEX)
|
||||
|
||||
# CSS3 defines both 'gray' and 'grey', as well as defining either
|
||||
# variant for other related colors like 'darkgray'/'darkgrey'. For a
|
||||
# 'forward' lookup from name to hex, this is straightforward, but a
|
||||
# 'reverse' lookup from hex to name requires picking one spelling.
|
||||
#
|
||||
# The way in which _reversedict() generates the reverse mappings will
|
||||
# pick a spelling based on the ordering of dictionary keys, which
|
||||
# varies according to the version and implementation of Python in use,
|
||||
# and in some Python versions is explicitly not to be relied on for
|
||||
# consistency. So here we manually pick a single spelling that will
|
||||
# consistently be returned. Since 'gray' was the only spelling
|
||||
# supported in HTML 4, CSS1, and CSS2, 'gray' and its varients are
|
||||
# chosen.
|
||||
CSS3_HEX_TO_NAMES["#a9a9a9"] = "darkgray"
|
||||
CSS3_HEX_TO_NAMES["#2f4f4f"] = "darkslategray"
|
||||
CSS3_HEX_TO_NAMES["#696969"] = "dimgray"
|
||||
CSS3_HEX_TO_NAMES["#808080"] = "gray"
|
||||
CSS3_HEX_TO_NAMES["#d3d3d3"] = "lightgray"
|
||||
CSS3_HEX_TO_NAMES["#778899"] = "lightslategray"
|
||||
CSS3_HEX_TO_NAMES["#708090"] = "slategray"
|
||||
|
||||
|
||||
# Normalization functions.
|
||||
#################################################################
|
||||
|
||||
|
||||
def normalize_hex(hex_value: str) -> str:
|
||||
"""
|
||||
Normalize a hexadecimal color value to 6 digits, lowercase.
|
||||
|
||||
"""
|
||||
match = HEX_COLOR_RE.match(hex_value)
|
||||
if match is None:
|
||||
raise ValueError(
|
||||
"'{}' is not a valid hexadecimal color value.".format(hex_value)
|
||||
)
|
||||
hex_digits = match.group(1)
|
||||
if len(hex_digits) == 3:
|
||||
hex_digits = "".join(2 * s for s in hex_digits)
|
||||
return "#{}".format(hex_digits.lower())
|
||||
|
||||
|
||||
def _normalize_integer_rgb(value: int) -> int:
|
||||
"""
|
||||
Internal normalization function for clipping integer values into
|
||||
the permitted range (0-255, inclusive).
|
||||
|
||||
"""
|
||||
return 0 if value < 0 else 255 if value > 255 else value
|
||||
|
||||
|
||||
def normalize_integer_triplet(rgb_triplet: IntTuple) -> IntegerRGB:
|
||||
"""
|
||||
Normalize an integer ``rgb()`` triplet so that all values are
|
||||
within the range 0-255 inclusive.
|
||||
|
||||
"""
|
||||
return IntegerRGB._make(_normalize_integer_rgb(value) for value in rgb_triplet)
|
||||
|
||||
|
||||
def _normalize_percent_rgb(value: str) -> str:
|
||||
"""
|
||||
Internal normalization function for clipping percent values into
|
||||
the permitted range (0%-100%, inclusive).
|
||||
|
||||
"""
|
||||
value = value.split("%")[0]
|
||||
percent = float(value) if "." in value else int(value)
|
||||
|
||||
return "0%" if percent < 0 else "100%" if percent > 100 else "{}%".format(percent)
|
||||
|
||||
|
||||
def normalize_percent_triplet(rgb_triplet: PercentTuple) -> PercentRGB:
|
||||
"""
|
||||
Normalize a percentage ``rgb()`` triplet so that all values are
|
||||
within the range 0%-100% inclusive.
|
||||
|
||||
"""
|
||||
return PercentRGB._make(_normalize_percent_rgb(value) for value in rgb_triplet)
|
||||
|
||||
|
||||
# Conversions from color names to various formats.
|
||||
#################################################################
|
||||
|
||||
|
||||
def name_to_hex(name: str, spec: str = CSS3) -> str:
|
||||
"""
|
||||
Convert a color name to a normalized hexadecimal color value.
|
||||
|
||||
The optional keyword argument ``spec`` determines which
|
||||
specification's list of color names will be used. The default is
|
||||
CSS3.
|
||||
|
||||
When no color of that name exists in the given specification,
|
||||
``ValueError`` is raised.
|
||||
|
||||
"""
|
||||
if spec not in SUPPORTED_SPECIFICATIONS:
|
||||
raise ValueError(SPECIFICATION_ERROR_TEMPLATE.format(spec=spec))
|
||||
normalized = name.lower()
|
||||
hex_value = {
|
||||
CSS2: CSS2_NAMES_TO_HEX,
|
||||
CSS21: CSS21_NAMES_TO_HEX,
|
||||
CSS3: CSS3_NAMES_TO_HEX,
|
||||
HTML4: HTML4_NAMES_TO_HEX,
|
||||
}[spec].get(normalized)
|
||||
if hex_value is None:
|
||||
raise ValueError(
|
||||
"'{name}' is not defined as a named color in {spec}".format(
|
||||
name=name, spec=spec
|
||||
)
|
||||
)
|
||||
return hex_value
|
||||
|
||||
|
||||
def name_to_rgb(name: str, spec: str = CSS3) -> IntegerRGB:
|
||||
"""
|
||||
Convert a color name to a 3-tuple of integers suitable for use in
|
||||
an ``rgb()`` triplet specifying that color.
|
||||
|
||||
"""
|
||||
return hex_to_rgb(name_to_hex(name, spec=spec))
|
||||
|
||||
|
||||
def name_to_rgb_percent(name: str, spec: str = CSS3) -> PercentRGB:
|
||||
"""
|
||||
Convert a color name to a 3-tuple of percentages suitable for use
|
||||
in an ``rgb()`` triplet specifying that color.
|
||||
|
||||
"""
|
||||
return rgb_to_rgb_percent(name_to_rgb(name, spec=spec))
|
||||
|
||||
|
||||
# Conversions from hexadecimal color values to various formats.
|
||||
#################################################################
|
||||
|
||||
|
||||
def hex_to_name(hex_value: str, spec: str = CSS3) -> str:
|
||||
"""
|
||||
Convert a hexadecimal color value to its corresponding normalized
|
||||
color name, if any such name exists.
|
||||
|
||||
The optional keyword argument ``spec`` determines which
|
||||
specification's list of color names will be used. The default is
|
||||
CSS3.
|
||||
|
||||
When no color name for the value is found in the given
|
||||
specification, ``ValueError`` is raised.
|
||||
|
||||
"""
|
||||
if spec not in SUPPORTED_SPECIFICATIONS:
|
||||
raise ValueError(SPECIFICATION_ERROR_TEMPLATE.format(spec=spec))
|
||||
normalized = normalize_hex(hex_value)
|
||||
name = {
|
||||
CSS2: CSS2_HEX_TO_NAMES,
|
||||
CSS21: CSS21_HEX_TO_NAMES,
|
||||
CSS3: CSS3_HEX_TO_NAMES,
|
||||
HTML4: HTML4_HEX_TO_NAMES,
|
||||
}[spec].get(normalized)
|
||||
if name is None:
|
||||
raise ValueError("'{}' has no defined color name in {}".format(hex_value, spec))
|
||||
return name
|
||||
|
||||
|
||||
def hex_to_rgb(hex_value: str) -> IntegerRGB:
|
||||
"""
|
||||
Convert a hexadecimal color value to a 3-tuple of integers
|
||||
suitable for use in an ``rgb()`` triplet specifying that color.
|
||||
|
||||
"""
|
||||
int_value = int(normalize_hex(hex_value)[1:], 16)
|
||||
return IntegerRGB(int_value >> 16, int_value >> 8 & 0xFF, int_value & 0xFF)
|
||||
|
||||
|
||||
def hex_to_rgb_percent(hex_value: str) -> PercentRGB:
|
||||
"""
|
||||
Convert a hexadecimal color value to a 3-tuple of percentages
|
||||
suitable for use in an ``rgb()`` triplet representing that color.
|
||||
|
||||
"""
|
||||
return rgb_to_rgb_percent(hex_to_rgb(hex_value))
|
||||
|
||||
|
||||
# Conversions from integer rgb() triplets to various formats.
|
||||
#################################################################
|
||||
|
||||
|
||||
def rgb_to_name(rgb_triplet: IntTuple, spec: str = CSS3) -> str:
|
||||
"""
|
||||
Convert a 3-tuple of integers, suitable for use in an ``rgb()``
|
||||
color triplet, to its corresponding normalized color name, if any
|
||||
such name exists.
|
||||
|
||||
The optional keyword argument ``spec`` determines which
|
||||
specification's list of color names will be used. The default is
|
||||
CSS3.
|
||||
|
||||
If there is no matching name, ``ValueError`` is raised.
|
||||
|
||||
"""
|
||||
return hex_to_name(rgb_to_hex(normalize_integer_triplet(rgb_triplet)), spec=spec)
|
||||
|
||||
|
||||
def rgb_to_hex(rgb_triplet: IntTuple) -> str:
|
||||
"""
|
||||
Convert a 3-tuple of integers, suitable for use in an ``rgb()``
|
||||
color triplet, to a normalized hexadecimal value for that color.
|
||||
|
||||
"""
|
||||
return "#{:02x}{:02x}{:02x}".format(*normalize_integer_triplet(rgb_triplet))
|
||||
|
||||
|
||||
def rgb_to_rgb_percent(rgb_triplet: IntTuple) -> PercentRGB:
|
||||
"""
|
||||
Convert a 3-tuple of integers, suitable for use in an ``rgb()``
|
||||
color triplet, to a 3-tuple of percentages suitable for use in
|
||||
representing that color.
|
||||
|
||||
This function makes some trade-offs in terms of the accuracy of
|
||||
the final representation; for some common integer values,
|
||||
special-case logic is used to ensure a precise result (e.g.,
|
||||
integer 128 will always convert to '50%', integer 32 will always
|
||||
convert to '12.5%'), but for all other values a standard Python
|
||||
``float`` is used and rounded to two decimal places, which may
|
||||
result in a loss of precision for some values.
|
||||
|
||||
"""
|
||||
# In order to maintain precision for common values,
|
||||
# special-case them.
|
||||
specials = {
|
||||
255: "100%",
|
||||
128: "50%",
|
||||
64: "25%",
|
||||
32: "12.5%",
|
||||
16: "6.25%",
|
||||
0: "0%",
|
||||
}
|
||||
return PercentRGB._make(
|
||||
specials.get(d, "{:.02f}%".format(d / 255.0 * 100))
|
||||
for d in normalize_integer_triplet(rgb_triplet)
|
||||
)
|
||||
|
||||
|
||||
# Conversions from percentage rgb() triplets to various formats.
|
||||
#################################################################
|
||||
|
||||
|
||||
def rgb_percent_to_name(rgb_percent_triplet: PercentTuple, spec: str = CSS3) -> str:
|
||||
"""
|
||||
Convert a 3-tuple of percentages, suitable for use in an ``rgb()``
|
||||
color triplet, to its corresponding normalized color name, if any
|
||||
such name exists.
|
||||
|
||||
The optional keyword argument ``spec`` determines which
|
||||
specification's list of color names will be used. The default is
|
||||
CSS3.
|
||||
|
||||
If there is no matching name, ``ValueError`` is raised.
|
||||
|
||||
"""
|
||||
return rgb_to_name(
|
||||
rgb_percent_to_rgb(normalize_percent_triplet(rgb_percent_triplet)), spec=spec
|
||||
)
|
||||
|
||||
|
||||
def rgb_percent_to_hex(rgb_percent_triplet: PercentTuple) -> str:
|
||||
"""
|
||||
Convert a 3-tuple of percentages, suitable for use in an ``rgb()``
|
||||
color triplet, to a normalized hexadecimal color value for that
|
||||
color.
|
||||
|
||||
"""
|
||||
return rgb_to_hex(
|
||||
rgb_percent_to_rgb(normalize_percent_triplet(rgb_percent_triplet))
|
||||
)
|
||||
|
||||
|
||||
def _percent_to_integer(percent: str) -> int:
|
||||
"""
|
||||
Internal helper for converting a percentage value to an integer
|
||||
between 0 and 255 inclusive.
|
||||
|
||||
"""
|
||||
return int(round(float(percent.split("%")[0]) / 100 * 255))
|
||||
|
||||
|
||||
def rgb_percent_to_rgb(rgb_percent_triplet: PercentTuple) -> IntegerRGB:
|
||||
"""
|
||||
Convert a 3-tuple of percentages, suitable for use in an ``rgb()``
|
||||
color triplet, to a 3-tuple of integers suitable for use in
|
||||
representing that color.
|
||||
|
||||
Some precision may be lost in this conversion. See the note
|
||||
regarding precision for ``rgb_to_rgb_percent()`` for details.
|
||||
|
||||
"""
|
||||
return IntegerRGB._make(
|
||||
map(_percent_to_integer, normalize_percent_triplet(rgb_percent_triplet))
|
||||
)
|
||||
|
||||
|
||||
# HTML5 color algorithms.
|
||||
#################################################################
|
||||
|
||||
# These functions are written in a way that may seem strange to
|
||||
# developers familiar with Python, because they do not use the most
|
||||
# efficient or idiomatic way of accomplishing their tasks. This is
|
||||
# because, for compliance, these functions are written as literal
|
||||
# translations into Python of the algorithms in HTML5.
|
||||
#
|
||||
# For ease of understanding, the relevant steps of the algorithm from
|
||||
# the standard are included as comments interspersed in the
|
||||
# implementation.
|
||||
|
||||
|
||||
def html5_parse_simple_color(input: str) -> HTML5SimpleColor:
|
||||
"""
|
||||
Apply the simple color parsing algorithm from section 2.4.6 of
|
||||
HTML5.
|
||||
|
||||
"""
|
||||
# 1. Let input be the string being parsed.
|
||||
#
|
||||
# 2. If input is not exactly seven characters long, then return an
|
||||
# error.
|
||||
if not isinstance(input, str) or len(input) != 7:
|
||||
raise ValueError(
|
||||
"An HTML5 simple color must be a Unicode string "
|
||||
"exactly seven characters long."
|
||||
)
|
||||
|
||||
# 3. If the first character in input is not a U+0023 NUMBER SIGN
|
||||
# character (#), then return an error.
|
||||
if not input.startswith("#"):
|
||||
raise ValueError(
|
||||
"An HTML5 simple color must begin with the " "character '#' (U+0023)."
|
||||
)
|
||||
|
||||
# 4. If the last six characters of input are not all ASCII hex
|
||||
# digits, then return an error.
|
||||
if not all(c in string.hexdigits for c in input[1:]):
|
||||
raise ValueError(
|
||||
"An HTML5 simple color must contain exactly six ASCII hex digits."
|
||||
)
|
||||
|
||||
# 5. Let result be a simple color.
|
||||
#
|
||||
# 6. Interpret the second and third characters as a hexadecimal
|
||||
# number and let the result be the red component of result.
|
||||
#
|
||||
# 7. Interpret the fourth and fifth characters as a hexadecimal
|
||||
# number and let the result be the green component of result.
|
||||
#
|
||||
# 8. Interpret the sixth and seventh characters as a hexadecimal
|
||||
# number and let the result be the blue component of result.
|
||||
#
|
||||
# 9. Return result.
|
||||
return HTML5SimpleColor(
|
||||
int(input[1:3], 16), int(input[3:5], 16), int(input[5:7], 16)
|
||||
)
|
||||
|
||||
|
||||
def html5_serialize_simple_color(simple_color: IntTuple) -> str:
|
||||
"""
|
||||
Apply the serialization algorithm for a simple color from section
|
||||
2.4.6 of HTML5.
|
||||
|
||||
"""
|
||||
red, green, blue = simple_color
|
||||
|
||||
# 1. Let result be a string consisting of a single "#" (U+0023)
|
||||
# character.
|
||||
result = "#"
|
||||
|
||||
# 2. Convert the red, green, and blue components in turn to
|
||||
# two-digit hexadecimal numbers using lowercase ASCII hex
|
||||
# digits, zero-padding if necessary, and append these numbers
|
||||
# to result, in the order red, green, blue.
|
||||
format_string = "{:02x}"
|
||||
result += format_string.format(red)
|
||||
result += format_string.format(green)
|
||||
result += format_string.format(blue)
|
||||
|
||||
# 3. Return result, which will be a valid lowercase simple color.
|
||||
return result
|
||||
|
||||
|
||||
def html5_parse_legacy_color(input: str) -> HTML5SimpleColor:
|
||||
"""
|
||||
Apply the legacy color parsing algorithm from section 2.4.6 of
|
||||
HTML5.
|
||||
|
||||
"""
|
||||
# 1. Let input be the string being parsed.
|
||||
if not isinstance(input, str):
|
||||
raise ValueError(
|
||||
"HTML5 legacy color parsing requires a Unicode string as input."
|
||||
)
|
||||
|
||||
# 2. If input is the empty string, then return an error.
|
||||
if input == "":
|
||||
raise ValueError("HTML5 legacy color parsing forbids empty string as a value.")
|
||||
|
||||
# 3. Strip leading and trailing whitespace from input.
|
||||
input = input.strip()
|
||||
|
||||
# 4. If input is an ASCII case-insensitive match for the string
|
||||
# "transparent", then return an error.
|
||||
if input.lower() == "transparent":
|
||||
raise ValueError('HTML5 legacy color parsing forbids "transparent" as a value.')
|
||||
|
||||
# 5. If input is an ASCII case-insensitive match for one of the
|
||||
# keywords listed in the SVG color keywords section of the CSS3
|
||||
# Color specification, then return the simple color
|
||||
# corresponding to that keyword.
|
||||
keyword_hex = CSS3_NAMES_TO_HEX.get(input.lower())
|
||||
if keyword_hex is not None:
|
||||
return html5_parse_simple_color(keyword_hex)
|
||||
|
||||
# 6. If input is four characters long, and the first character in
|
||||
# input is a "#" (U+0023) character, and the last three
|
||||
# characters of input are all ASCII hex digits, then run these
|
||||
# substeps:
|
||||
if (
|
||||
len(input) == 4
|
||||
and input.startswith("#")
|
||||
and all(c in string.hexdigits for c in input[1:])
|
||||
):
|
||||
# 1. Let result be a simple color.
|
||||
#
|
||||
# 2. Interpret the second character of input as a hexadecimal
|
||||
# digit; let the red component of result be the resulting
|
||||
# number multiplied by 17.
|
||||
#
|
||||
# 3. Interpret the third character of input as a hexadecimal
|
||||
# digit; let the green component of result be the resulting
|
||||
# number multiplied by 17.
|
||||
#
|
||||
# 4. Interpret the fourth character of input as a hexadecimal
|
||||
# digit; let the blue component of result be the resulting
|
||||
# number multiplied by 17.
|
||||
result = HTML5SimpleColor(
|
||||
int(input[1], 16) * 17, int(input[2], 16) * 17, int(input[3], 16) * 17
|
||||
)
|
||||
|
||||
# 5. Return result.
|
||||
return result
|
||||
|
||||
# 7. Replace any characters in input that have a Unicode code
|
||||
# point greater than U+FFFF (i.e. any characters that are not
|
||||
# in the basic multilingual plane) with the two-character
|
||||
# string "00".
|
||||
input = "".join("00" if ord(c) > 0xFFFF else c for c in input)
|
||||
|
||||
# 8. If input is longer than 128 characters, truncate input,
|
||||
# leaving only the first 128 characters.
|
||||
if len(input) > 128:
|
||||
input = input[:128]
|
||||
|
||||
# 9. If the first character in input is a "#" (U+0023) character,
|
||||
# remove it.
|
||||
if input.startswith("#"):
|
||||
input = input[1:]
|
||||
|
||||
# 10. Replace any character in input that is not an ASCII hex
|
||||
# digit with the character "0" (U+0030).
|
||||
input = "".join(c if c in string.hexdigits else "0" for c in input)
|
||||
|
||||
# 11. While input's length is zero or not a multiple of three,
|
||||
# append a "0" (U+0030) character to input.
|
||||
while (len(input) == 0) or (len(input) % 3 != 0):
|
||||
input += "0"
|
||||
|
||||
# 12. Split input into three strings of equal length, to obtain
|
||||
# three components. Let length be the length of those
|
||||
# components (one third the length of input).
|
||||
length = int(len(input) / 3)
|
||||
red = input[:length]
|
||||
green = input[length : length * 2]
|
||||
blue = input[length * 2 :]
|
||||
|
||||
# 13. If length is greater than 8, then remove the leading
|
||||
# length-8 characters in each component, and let length be 8.
|
||||
if length > 8:
|
||||
red, green, blue = (red[length - 8 :], green[length - 8 :], blue[length - 8 :])
|
||||
length = 8
|
||||
|
||||
# 14. While length is greater than two and the first character in
|
||||
# each component is a "0" (U+0030) character, remove that
|
||||
# character and reduce length by one.
|
||||
while (length > 2) and (red[0] == "0" and green[0] == "0" and blue[0] == "0"):
|
||||
red, green, blue = (red[1:], green[1:], blue[1:])
|
||||
length -= 1
|
||||
|
||||
# 15. If length is still greater than two, truncate each
|
||||
# component, leaving only the first two characters in each.
|
||||
if length > 2:
|
||||
red, green, blue = (red[:2], green[:2], blue[:2])
|
||||
|
||||
# 16. Let result be a simple color.
|
||||
#
|
||||
# 17. Interpret the first component as a hexadecimal number; let
|
||||
# the red component of result be the resulting number.
|
||||
#
|
||||
# 18. Interpret the second component as a hexadecimal number; let
|
||||
# the green component of result be the resulting number.
|
||||
#
|
||||
# 19. Interpret the third component as a hexadecimal number; let
|
||||
# the blue component of result be the resulting number.
|
||||
#
|
||||
# 20. Return result.
|
||||
return HTML5SimpleColor(int(red, 16), int(green, 16), int(blue, 16))
|
@ -1 +0,0 @@
|
||||
pip
|
@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019 joe tats
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user