mirror of
https://github.com/OpenVoiceOS/OpenVoiceOS
synced 2025-04-02 20:51:07 +02: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"]
|
[submodule "buildroot"]
|
||||||
path = buildroot
|
path = buildroot
|
||||||
url = https://github.com/buildroot/buildroot.git
|
url = https://github.com/j1nx/buildroot.git
|
||||||
branch = 2023.02.x
|
branch = ovos-2023.02.x
|
||||||
|
6
Makefile
6
Makefile
@ -39,6 +39,12 @@ clean:
|
|||||||
menuconfig:
|
menuconfig:
|
||||||
$(MAKE) -C $(BUILDROOT) BR2_EXTERNAL=../$(BUILDROOT_EXTERNAL) 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:
|
savedefconfig:
|
||||||
$(MAKE) -C $(BUILDROOT) BR2_EXTERNAL=../$(BUILDROOT_EXTERNAL) savedefconfig
|
$(MAKE) -C $(BUILDROOT) BR2_EXTERNAL=../$(BUILDROOT_EXTERNAL) savedefconfig
|
||||||
|
|
||||||
|
@ -2,5 +2,11 @@ set default="0"
|
|||||||
set timeout="3"
|
set timeout="3"
|
||||||
|
|
||||||
menuentry "OpenVoiceOS" {
|
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_BTRFS_FS=m
|
||||||
CONFIG_OVERLAY_FS=y
|
CONFIG_OVERLAY_FS=y
|
||||||
|
|
||||||
CONFIG_SECCOMP=y
|
# CONFIG_SECCOMP is not set
|
||||||
CONFIG_SECCOMP_FILTER=y
|
# CONFIG_AUDIT is not set
|
||||||
|
# CONFIG_SECURITY is not set
|
||||||
|
# CONFIG_SECURITY_SELINUX is not set
|
||||||
|
|
||||||
CONFIG_CRYPTO=y
|
CONFIG_CRYPTO=y
|
||||||
CONFIG_CRYPTO_LZ4=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